sys-admin 1.6.4 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,997 +1,997 @@
1
- require 'ffi'
2
- require 'win32ole'
3
- require 'win32/security'
4
- require 'win32/registry'
5
- require 'socket'
6
-
7
- module Sys
8
- class Group
9
- # Short description of the object.
10
- attr_accessor :caption
11
-
12
- # Description of the group.
13
- attr_accessor :description
14
-
15
- # Name of the Windows domain to which the group account belongs.
16
- attr_accessor :domain
17
-
18
- # Date the group was added.
19
- attr_accessor :install_date
20
-
21
- # Name of the Windows group account on the Group#domain specified.
22
- attr_accessor :name
23
-
24
- # Security identifier for this group.
25
- attr_accessor :sid
26
-
27
- # Current status for the group, such as "ok", "error", etc.
28
- attr_accessor :status
29
-
30
- # The group ID.
31
- attr_accessor :gid
32
-
33
- # Sets whether or not the group is local (as opposed to global).
34
- attr_writer :local
35
-
36
- # An array of members for that group. May contain SID's.
37
- attr_accessor :members
38
-
39
- # Creates and returns a new Group object. This class encapsulates
40
- # the information for a group account, whether it be global or local.
41
- #
42
- # Yields +self+ if a block is given.
43
- #
44
- def initialize
45
- yield self if block_given?
46
- end
47
-
48
- # Returns whether or not the group is a local group.
49
- #
50
- def local?
51
- @local
52
- end
53
-
54
- # Returns the type of SID (Security Identifier) as a stringified value.
55
- #
56
- def sid_type
57
- @sid_type
58
- end
59
-
60
- # Sets the SID (Security Identifier) type to +stype+, which can be
61
- # one of the following constant values:
62
- #
63
- # * Admin::SidTypeUser
64
- # * Admin::SidTypeGroup
65
- # * Admin::SidTypeDomain
66
- # * Admin::SidTypeAlias
67
- # * Admin::SidTypeWellKnownGroup
68
- # * Admin::SidTypeDeletedAccount
69
- # * Admin::SidTypeInvalid
70
- # * Admin::SidTypeUnknown
71
- # * Admin::SidTypeComputer
72
- #
73
- def sid_type=(stype)
74
- if stype.kind_of?(String)
75
- @sid_type = stype.downcase
76
- else
77
- case stype
78
- when Admin::SidTypeUser
79
- @sid_type = "user"
80
- when Admin::SidTypeGroup
81
- @sid_type = "group"
82
- when Admin::SidTypeDomain
83
- @sid_type = "domain"
84
- when Admin::SidTypeAlias
85
- @sid_type = "alias"
86
- when Admin::SidTypeWellKnownGroup
87
- @sid_type = "well_known_group"
88
- when Admin::SidTypeDeletedAccount
89
- @sid_type = "deleted_account"
90
- when Admin::SidTypeInvalid
91
- @sid_type = "invalid"
92
- when Admin::SidTypeUnknown
93
- @sid_type = "unknown"
94
- when Admin::SidTypeComputer
95
- @sid_type = "computer"
96
- else
97
- @sid_type = "unknown"
98
- end
99
- end
100
-
101
- @sid_type
102
- end
103
- end
104
-
105
- class User
106
- # An account for users whose primary account is in another domain.
107
- TEMP_DUPLICATE = 0x0100
108
-
109
- # Default account type that represents a typical user.
110
- NORMAL = 0x0200
111
-
112
- # A permit to trust account for a domain that trusts other domains.
113
- INTERDOMAIN_TRUST = 0x0800
114
-
115
- # An account for a Windows NT/2000 workstation or server that is a
116
- # member of this domain.
117
- WORKSTATION_TRUST = 0x1000
118
-
119
- # A computer account for a backup domain controller that is a member
120
- # of this domain.
121
- SERVER_TRUST = 0x2000
122
-
123
- # Domain and username of the account.
124
- attr_accessor :caption
125
-
126
- # Description of the account.
127
- attr_accessor :description
128
-
129
- # Name of the Windows domain to which a user account belongs.
130
- attr_accessor :domain
131
-
132
- # The user's password.
133
- attr_accessor :password
134
-
135
- # Full name of a local user.
136
- attr_accessor :full_name
137
-
138
- # An array of groups to which the user belongs.
139
- attr_accessor :groups
140
-
141
- # Date the user account was created.
142
- attr_accessor :install_date
143
-
144
- # Name of the Windows user account on the domain that the User#domain
145
- # property specifies.
146
- attr_accessor :name
147
-
148
- # The user's security identifier.
149
- attr_accessor :sid
150
-
151
- # Current status for the user, such as "ok", "error", etc.
152
- attr_accessor :status
153
-
154
- # The user's id (RID).
155
- attr_accessor :uid
156
-
157
- # The user's primary group ID.
158
- attr_accessor :gid
159
-
160
- # The user's home directory
161
- attr_accessor :dir
162
-
163
- # Used to set whether or not the account is disabled.
164
- attr_writer :disabled
165
-
166
- # Sets whether or not the account is defined on the local computer.
167
- attr_writer :local
168
-
169
- # Sets whether or not the account is locked out of the OS.
170
- attr_writer :lockout
171
-
172
- # Sets whether or not the password for the account can be changed.
173
- attr_writer :password_changeable
174
-
175
- # Sets whether or not the password for the account expires.
176
- attr_writer :password_expires
177
-
178
- # Sets whether or not a password is required for the account.
179
- attr_writer :password_required
180
-
181
- # Returns the account type as a human readable string.
182
- attr_reader :account_type
183
-
184
- # Creates an returns a new User object. A User object encapsulates a
185
- # user account on the operating system.
186
- #
187
- # Yields +self+ if a block is provided.
188
- #
189
- def initialize
190
- yield self if block_given?
191
- end
192
-
193
- # Sets the account type for the account. Possible values are:
194
- #
195
- # * User::TEMP_DUPLICATE
196
- # * User::NORMAL
197
- # * User::INTERDOMAIN_TRUST
198
- # * User::WORKSTATION_TRUST
199
- # * User::SERVER_TRUST
200
- #
201
- def account_type=(type)
202
- case type
203
- when TEMP_DUPLICATE
204
- @account_type = 'duplicate'
205
- when NORMAL
206
- @account_type = 'normal'
207
- when INTERDOMAIN_TRUST
208
- @account_type = 'interdomain_trust'
209
- when WORKSTATION_TRUST
210
- @account_type = 'workstation_trust'
211
- when SERVER_TRUST
212
- @account_type = 'server_trust'
213
- else
214
- @account_type = 'unknown'
215
- end
216
- end
217
-
218
- # Returns the SID type as a human readable string.
219
- #
220
- def sid_type
221
- @sid_type
222
- end
223
-
224
- # Sets the SID (Security Identifier) type to +stype+, which can be
225
- # one of the following constant values:
226
- #
227
- # * Admin::SidTypeUser
228
- # * Admin::SidTypeGroup
229
- # * Admin::SidTypeDomain
230
- # * Admin::SidTypeAlias
231
- # * Admin::SidTypeWellKnownGroup
232
- # * Admin::SidTypeDeletedAccount
233
- # * Admin::SidTypeInvalid
234
- # * Admin::SidTypeUnknown
235
- # * Admin::SidTypeComputer
236
- #
237
- def sid_type=(stype)
238
- case stype
239
- when Admin::SidTypeUser
240
- @sid_type = 'user'
241
- when Admin::SidTypeGroup
242
- @sid_type = 'group'
243
- when Admin::SidTypeDomain
244
- @sid_type = 'domain'
245
- when Admin::SidTypeAlias
246
- @sid_type = 'alias'
247
- when Admin::SidTypeWellKnownGroup
248
- @sid_type = 'well_known_group'
249
- when Admin::SidTypeDeletedAccount
250
- @sid_type = 'deleted_account'
251
- when Admin::SidTypeInvalid
252
- @sid_type = 'invalid'
253
- when Admin::SidTypeUnknown
254
- @sid_type = 'unknown'
255
- when Admin::SidTypeComputer
256
- @sid_type = 'computer'
257
- else
258
- @sid_type = 'unknown'
259
- end
260
- end
261
-
262
- # Returns whether or not the account is disabled.
263
- #
264
- def disabled?
265
- @disabled
266
- end
267
-
268
- # Returns whether or not the account is local.
269
- #
270
- def local?
271
- @local
272
- end
273
-
274
- # Returns whether or not the account is locked out.
275
- #
276
- def lockout?
277
- @lockout
278
- end
279
-
280
- # Returns whether or not the password for the account is changeable.
281
- #
282
- def password_changeable?
283
- @password_changeable
284
- end
285
-
286
- # Returns whether or not the password for the account is changeable.
287
- #
288
- def password_expires?
289
- @password_expires
290
- end
291
-
292
- # Returns whether or not the a password is required for the account.
293
- #
294
- def password_required?
295
- @password_required
296
- end
297
- end
298
-
299
- class Admin
300
- extend FFI::Library
301
-
302
- # This is the error raised in the majority of cases if anything goes wrong
303
- # with any of the Sys::Admin methods.
304
- #
305
- class Error < StandardError; end
306
-
307
- SidTypeUser = 1
308
- SidTypeGroup = 2
309
- SidTypeDomain = 3
310
- SidTypeAlias = 4
311
- SidTypeWellKnownGroup = 5
312
- SidTypeDeletedAccount = 6
313
- SidTypeInvalid = 7
314
- SidTypeUnknown = 8
315
- SidTypeComputer = 9
316
-
317
- private
318
-
319
- HKEY = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\"
320
-
321
- # Retrieves the user's home directory. For local accounts query the
322
- # registry. For domain accounts use ADSI and use the HomeDirectory.
323
- #
324
- def self.get_home_dir(user, local = false, domain = nil)
325
- if local
326
- sec = Win32::Security::SID.open(user)
327
- key = HKEY + sec.to_s
328
- dir = nil
329
-
330
- begin
331
- Win32::Registry::HKEY_LOCAL_MACHINE.open(key) do |reg|
332
- dir = reg['ProfileImagePath']
333
- end
334
- rescue Win32::Registry::Error
335
- # Not every local user has a home directory
336
- end
337
- else
338
- domain ||= Socket.gethostname
339
- adsi = WIN32OLE.connect("WinNT://#{domain}/#{user},user")
340
- dir = adsi.get('HomeDirectory')
341
- end
342
-
343
- dir
344
- end
345
-
346
- # A private method that lower cases all keys, and converts them
347
- # all to symbols.
348
- #
349
- def self.munge_options(opts)
350
- rhash = {}
351
-
352
- opts.each{ |k, v|
353
- k = k.to_s.downcase.to_sym
354
- rhash[k] = v
355
- }
356
-
357
- rhash
358
- end
359
-
360
- # An internal, private method for getting a list of groups for
361
- # a particular user. The first member is a list of group names,
362
- # the second member is the primary group ID.
363
- #
364
- def self.get_groups(domain, user)
365
- array = []
366
- adsi = WIN32OLE.connect("WinNT://#{domain}/#{user},user")
367
- adsi.groups.each{ |g| array << g.name }
368
- [array, adsi.PrimaryGroupId]
369
- end
370
-
371
- # An internal, private method for getting a list of members for
372
- # any particular group.
373
- #
374
- def self.get_members(domain, group)
375
- array = []
376
- adsi = WIN32OLE.connect("WinNT://#{domain}/#{group}")
377
- adsi.members.each{ |g| array << g.name }
378
- array
379
- end
380
-
381
- # Used by the get_login method
382
- ffi_lib :advapi32
383
- attach_function :GetUserNameW, [:pointer, :pointer], :bool
384
- private_class_method :GetUserNameW
385
-
386
- public
387
-
388
- # Creates the given +user+. If no domain option is specified,
389
- # then it defaults to your local host, i.e. a local account is
390
- # created.
391
- #
392
- # Any options provided are treated as IADsUser interface methods
393
- # and are called before SetInfo is finally called.
394
- #
395
- # Examples:
396
- #
397
- # # Create a local user with no options
398
- # Sys::Admin.add_user(:name => 'asmith')
399
- #
400
- # # Create a local user with options
401
- # Sys::Admin.add_user(
402
- # :name => 'asmith',
403
- # :description => 'Really cool guy',
404
- # :password => 'abc123'
405
- # )
406
- #
407
- # # Create a user on a specific domain
408
- # Sys::Admin.add_user(
409
- # :name => 'asmith',
410
- # :domain => 'XX',
411
- # :fullname => 'Al Smith'
412
- # )
413
- #--
414
- # Most options are passed to the 'put' method. However, we handle the
415
- # password specially since it's a separate method, and some environments
416
- # require that it be set up front.
417
- #
418
- def self.add_user(options = {})
419
- options = munge_options(options)
420
-
421
- name = options.delete(:name) or raise ArgumentError, 'No user given'
422
- domain = options[:domain]
423
-
424
- if domain.nil?
425
- domain = Socket.gethostname
426
- moniker = "WinNT://#{domain},Computer"
427
- else
428
- moniker = "WinNT://#{domain}"
429
- end
430
-
431
- begin
432
- adsi = WIN32OLE.connect(moniker)
433
- user = adsi.create('user', name)
434
-
435
- options.each{ |option, value|
436
- if option.to_s == 'password'
437
- user.setpassword(value)
438
- else
439
- user.put(option.to_s, value)
440
- end
441
- }
442
-
443
- user.setinfo
444
- rescue WIN32OLERuntimeError => err
445
- raise Error, err
446
- end
447
- end
448
-
449
- # Configures the +user+ using +options+. If no domain option is
450
- # specified then your local host is used, i.e. you are configuring
451
- # a local user account.
452
- #
453
- # See http://tinyurl.com/3hjv9 for a list of valid options.
454
- #
455
- # In the case of a password change, pass a two element array as the
456
- # old value and new value.
457
- #
458
- # Examples:
459
- #
460
- # # Configure a local user
461
- # Sys::Admin.configure_user(
462
- # :name => 'djberge',
463
- # :description => 'Awesome'
464
- # )
465
- #
466
- # # Change the password
467
- # Sys::Admin.configure_user(
468
- # :name => 'asmith',
469
- # :password => 'new_password'
470
- # )
471
- #
472
- # # Configure a user on a specific domain
473
- # Sys::Admin.configure_user(
474
- # :name => 'jsmrz',
475
- # :domain => 'XX',
476
- # :firstname => 'Jo'
477
- # )
478
- #
479
- def self.configure_user(options = {})
480
- options = munge_options(options)
481
-
482
- name = options.delete(:name) or raise ArgumentError, 'No name given'
483
- domain = options[:domain] || Socket.gethostname
484
-
485
- begin
486
- adsi = WIN32OLE.connect("WinNT://#{domain}/#{name},user")
487
-
488
- options.each{ |option, value|
489
- if option.to_s == 'password'
490
- adsi.setpassword(value)
491
- else
492
- adsi.put(option.to_s, value)
493
- end
494
- }
495
-
496
- adsi.setinfo
497
- rescue WIN32OLERuntimeError => err
498
- raise Error, err
499
- end
500
- end
501
-
502
- # Deletes the given +user+ on +domain+. If no domain is specified,
503
- # then it defaults to your local host, i.e. a local account is
504
- # deleted.
505
- #
506
- def self.delete_user(user, domain = nil)
507
- if domain.nil?
508
- domain = Socket.gethostname
509
- moniker = "WinNT://#{domain},Computer"
510
- else
511
- moniker = "WinNT://#{domain}"
512
- end
513
-
514
- begin
515
- adsi = WIN32OLE.connect(moniker)
516
- adsi.delete('user', user)
517
- rescue WIN32OLERuntimeError => err
518
- raise Error, err
519
- end
520
- end
521
-
522
- # Create a new +group+ using +options+. If no domain option is specified
523
- # then a local group is created instead.
524
- #
525
- # Examples:
526
- #
527
- # # Create a local group with no options
528
- # Sys::Admin.add_group(:name => 'Dudes')
529
- #
530
- # # Create a local group with options
531
- # Sys::Admin.add_group(:name => 'Dudes', :description => 'Boys')
532
- #
533
- # # Create a group on a specific domain
534
- # Sys::Admin.add_group(
535
- # :name => 'Ladies',
536
- # :domain => 'XYZ',
537
- # :description => 'Girls'
538
- # )
539
- #
540
- def self.add_group(options = {})
541
- options = munge_options(options)
542
-
543
- group = options.delete(:name) or raise ArgumentError, 'No name given'
544
- domain = options[:domain]
545
-
546
- if domain.nil?
547
- domain = Socket.gethostname
548
- moniker = "WinNT://#{domain},Computer"
549
- else
550
- moniker = "WinNT://#{domain}"
551
- end
552
-
553
- begin
554
- adsi = WIN32OLE.connect(moniker)
555
- group = adsi.create('group', group)
556
- group.setinfo
557
- configure_group(options) unless options.empty?
558
- rescue WIN32OLERuntimeError => err
559
- raise Error, err
560
- end
561
- end
562
-
563
- # Adds +user+ to +group+ on the specified +domain+, or the localhost
564
- # if no domain is specified.
565
- #
566
- def self.add_group_member(user, group, domain=nil)
567
- domain ||= Socket.gethostname
568
- adsi = WIN32OLE.connect("WinNT://#{domain}/#{group},group")
569
- adsi.Add("WinNT://#{domain}/#{user}")
570
- rescue WIN32OLERuntimeError => err
571
- raise Error, err
572
- end
573
-
574
- # Removes +user+ from +group+ on the specified +domain+, or the localhost
575
- # if no domain is specified.
576
- #
577
- def self.remove_group_member(user, group, domain=nil)
578
- domain ||= Socket.gethostname
579
- adsi = WIN32OLE.connect("WinNT://#{domain}/#{group},group")
580
- adsi.Remove("WinNT://#{domain}/#{user}")
581
- rescue WIN32OLERuntimeError => err
582
- raise Error, err
583
- end
584
-
585
- # Configures the +group+ using +options+. If no domain option is
586
- # specified then your local host is used, i.e. you are configuring
587
- # a local group.
588
- #
589
- # See http://tinyurl.com/cjkzl for a list of valid options.
590
- #
591
- # Examples:
592
- #
593
- # # Configure a local group.
594
- # Sys::Admin.configure_group(:name => 'Abba', :description => 'Swedish')
595
- #
596
- # # Configure a group on a specific domain.
597
- # Sys::Admin.configure_group(
598
- # :name => 'Web Team',
599
- # :domain => 'Foo',
600
- # :description => 'Web programming cowboys'
601
- # )
602
- #
603
- def self.configure_group(options = {})
604
- options = munge_options(options)
605
-
606
- group = options.delete(:name) or raise ArgumentError, 'No name given'
607
- domain = options[:domain] || Socket.gethostname
608
-
609
- begin
610
- adsi = WIN32OLE.connect("WinNT://#{domain}/#{group},group")
611
- options.each{ |option, value| adsi.put(option.to_s, value) }
612
- adsi.setinfo
613
- rescue WIN32OLERuntimeError => err
614
- raise Error, err
615
- end
616
- end
617
-
618
- # Delete the +group+ from +domain+. If no domain is specified, then
619
- # you are deleting a local group.
620
- #
621
- def self.delete_group(group, domain = nil)
622
- if domain.nil?
623
- domain = Socket.gethostname
624
- moniker = "WinNT://#{domain},Computer"
625
- else
626
- moniker = "WinNT://#{domain}"
627
- end
628
-
629
- begin
630
- adsi = WIN32OLE.connect(moniker)
631
- adsi.delete('group', group)
632
- rescue WIN32OLERuntimeError => err
633
- raise Error, err
634
- end
635
- end
636
-
637
- # Returns the user name (only) of the current login.
638
- #
639
- def self.get_login
640
- buffer = FFI::MemoryPointer.new(:char, 256)
641
- nsize = FFI::MemoryPointer.new(:ulong)
642
- nsize.write_ulong(buffer.size)
643
-
644
- unless GetUserNameW(buffer, nsize)
645
- raise Error, 'GetUserName() call failed in get_login'
646
- end
647
-
648
- buffer.read_string(nsize.read_ulong * 2).tr(0.chr, '')
649
- end
650
-
651
- # Returns a User object based on either +name+ or +uid+.
652
- #
653
- # call-seq:
654
- # Sys::Admin.get_user(name, options = {})
655
- # Sys::Admin.get_user(uid, options = {})
656
- #
657
- # Looks for +usr+ information based on the options you specify, where
658
- # the +usr+ argument can be either a user name or a RID.
659
- #
660
- # If a 'host' option is specified, information is retrieved from that
661
- # host. Otherwise, the local host is used.
662
- #
663
- # All other options are converted to WQL statements against the
664
- # Win32_UserAccount WMI object. See http://tinyurl.com/by9nvn for a
665
- # list of possible options.
666
- #
667
- # Examples:
668
- #
669
- # # Get a user by name
670
- # Admin.get_user('djberge')
671
- #
672
- # # Get a user by uid
673
- # Admin.get_user(100)
674
- #
675
- # # Get a user on a specific domain
676
- # Admin.get_user('djberge', :domain => 'TEST')
677
- #--
678
- # The reason for keeping the +usr+ variable as a separate argument
679
- # instead of rolling it into the options hash was to keep a unified
680
- # API between the Windows and UNIX versions.
681
- #
682
- def self.get_user(usr, options = {})
683
- options = munge_options(options)
684
-
685
- host = options.delete(:host) || Socket.gethostname
686
- cs = "winmgmts:{impersonationLevel=impersonate}!"
687
- cs << "//#{host}/root/cimv2"
688
-
689
- begin
690
- wmi = WIN32OLE.connect(cs)
691
- rescue WIN32OLERuntimeError => err
692
- raise Error, err
693
- end
694
-
695
- query = "select * from win32_useraccount"
696
-
697
- i = 0
698
-
699
- options.each{ |opt, val|
700
- if i == 0
701
- query << " where #{opt} = '#{val}'"
702
- i += 1
703
- else
704
- query << " and #{opt} = '#{val}'"
705
- end
706
- }
707
-
708
- if usr.kind_of?(Fixnum)
709
- query << " and sid like '%-#{usr}'"
710
- else
711
- query << " and name = '#{usr}'"
712
- end
713
-
714
- domain = options[:domain] || host
715
-
716
- wmi.execquery(query).each{ |user|
717
- uid = user.sid.split('-').last.to_i
718
-
719
- # Because our 'like' query isn't fulproof, let's parse
720
- # the SID again to make sure
721
- if usr.kind_of?(Fixnum)
722
- next if usr != uid
723
- end
724
-
725
- groups, primary_group = *get_groups(domain, user.name)
726
-
727
- user_object = User.new do |u|
728
- u.account_type = user.accounttype
729
- u.caption = user.caption
730
- u.description = user.description
731
- u.disabled = user.disabled
732
- u.domain = user.domain
733
- u.full_name = user.fullname
734
- u.install_date = user.installdate
735
- u.local = user.localaccount
736
- u.lockout = user.lockout
737
- u.name = user.name
738
- u.password_changeable = user.passwordchangeable
739
- u.password_expires = user.passwordexpires
740
- u.password_required = user.passwordrequired
741
- u.sid = user.sid
742
- u.sid_type = user.sidtype
743
- u.status = user.status
744
- u.uid = uid
745
- u.gid = primary_group
746
- u.groups = groups
747
- u.dir = get_home_dir(user.name, options[:localaccount], domain)
748
- end
749
-
750
- return user_object
751
- }
752
-
753
- # If we're here, it means it wasn't found.
754
- raise Error, "no user found for '#{usr}'"
755
- end
756
-
757
- # Returns an array of User objects for each user on the system.
758
- #
759
- # You may specify a host from which information is retrieved. The
760
- # default is the local host.
761
- #
762
- # All other arguments are passed as WQL query parameters against
763
- # the Win32_UserAccont WMI object.
764
- #
765
- # Examples:
766
- #
767
- # # Get all local account users
768
- # Sys::Admin.users(:localaccount => true)
769
- #
770
- # # Get all user accounts on a specific domain
771
- # Sys::Admin.users(:domain => 'FOO')
772
- #
773
- # # Get a single user from a domain
774
- # Sys::Admin.users(:name => 'djberge', :domain => 'FOO')
775
- #
776
- def self.users(options = {})
777
- options = munge_options(options)
778
-
779
- host = options.delete(:host) || Socket.gethostname
780
- cs = "winmgmts:{impersonationLevel=impersonate}!"
781
- cs << "//#{host}/root/cimv2"
782
-
783
- begin
784
- wmi = WIN32OLE.connect(cs)
785
- rescue WIN32OLERuntimeError => e
786
- raise Error, e
787
- end
788
-
789
- query = "select * from win32_useraccount"
790
-
791
- i = 0
792
-
793
- options.each{ |opt, val|
794
- if i == 0
795
- query << " where #{opt} = '#{val}'"
796
- i += 1
797
- else
798
- query << " and #{opt} = '#{val}'"
799
- end
800
- }
801
-
802
- array = []
803
- domain = options[:domain] || host
804
-
805
- wmi.execquery(query).each{ |user|
806
- uid = user.sid.split('-').last.to_i
807
-
808
- usr = User.new do |u|
809
- u.account_type = user.accounttype
810
- u.caption = user.caption
811
- u.description = user.description
812
- u.disabled = user.disabled
813
- u.domain = user.domain
814
- u.full_name = user.fullname
815
- u.install_date = user.installdate
816
- u.local = user.localaccount
817
- u.lockout = user.lockout
818
- u.name = user.name
819
- u.password_changeable = user.passwordchangeable
820
- u.password_expires = user.passwordexpires
821
- u.password_required = user.passwordrequired
822
- u.sid = user.sid
823
- u.sid_type = user.sidtype
824
- u.status = user.status
825
- u.groups = get_groups(domain, user.name)
826
- u.uid = uid
827
- u.dir = get_home_dir(user.name, options[:localaccount], host)
828
- end
829
-
830
- array.push(usr)
831
- }
832
-
833
- array
834
- end
835
-
836
- # Returns a Group object based on either +name+ or +gid+.
837
- #
838
- # call-seq:
839
- # Sys::Admin.get_group(name, options = {})
840
- # Sys::Admin.get_group(gid, options = {})
841
- #
842
- # If a numeric value is sent as the first parameter, it is treated
843
- # as a RID and is checked against the SID for a match.
844
- #
845
- # You may specify a host as an option from which information is
846
- # retrieved. The default is the local host.
847
- #
848
- # All other options are passed as WQL parameters to the Win32_Group
849
- # WMI object. See http://tinyurl.com/bngc8s for a list of possible
850
- # options.
851
- #
852
- # Examples:
853
- #
854
- # # Find a group by name
855
- # Sys::Admin.get_group('Web Team')
856
- #
857
- # # Find a group by id
858
- # Sys::Admin.get_group(31667)
859
- #
860
- # # Find a group on a specific domain
861
- # Sys::Admin.get_group('Web Team', :domain => 'FOO')
862
- #
863
- def self.get_group(grp, options = {})
864
- options = munge_options(options)
865
-
866
- host = options.delete(:host) || Socket.gethostname
867
- cs = "winmgmts:{impersonationLevel=impersonate}!"
868
- cs << "//#{host}/root/cimv2"
869
-
870
- begin
871
- wmi = WIN32OLE.connect(cs)
872
- rescue WIN32OLERuntimeError => err
873
- raise Error, err
874
- end
875
-
876
- query = "select * from win32_group"
877
-
878
- i = 0
879
-
880
- options.each{ |opt, val|
881
- if i == 0
882
- query << " where #{opt} = '#{val}'"
883
- i += 1
884
- else
885
- query << " and #{opt} = '#{val}'"
886
- end
887
- }
888
-
889
- if grp.kind_of?(Fixnum)
890
- query << " and sid like '%-#{grp}'"
891
- else
892
- query << " and name = '#{grp}'"
893
- end
894
-
895
- domain = options[:domain] || host
896
-
897
- wmi.execquery(query).each{ |group|
898
- gid = group.sid.split("-").last.to_i
899
-
900
- # Because our 'like' query isn't fulproof, let's parse
901
- # the SID again to make sure
902
- if grp.kind_of?(Fixnum)
903
- next if grp != gid
904
- end
905
-
906
- group_object = Group.new do |g|
907
- g.caption = group.caption
908
- g.description = group.description
909
- g.domain = group.domain
910
- g.gid = gid
911
- g.install_date = group.installdate
912
- g.local = group.localaccount
913
- g.name = group.name
914
- g.sid = group.sid
915
- g.sid_type = group.sidtype
916
- g.status = group.status
917
- g.members = get_members(domain, group.name)
918
- end
919
-
920
- return group_object
921
- }
922
-
923
- # If we're here, it means it wasn't found.
924
- raise Error, "no group found for '#{grp}'"
925
- end
926
-
927
- # Returns an array of Group objects for each user on the system.
928
- #
929
- # You may specify a host option from which information is retrieved.
930
- # The default is the local host.
931
- #
932
- # All other options are passed as WQL parameters to the Win32_Group
933
- # WMI object. See http://tinyurl.com/bngc8s for a list of possible
934
- # options.
935
- #
936
- # Examples:
937
- #
938
- # # Get local group information
939
- # Sys::Admin.groups(:localaccount => true)
940
- #
941
- # # Get all groups on a specific domain
942
- # Sys::Admin.groups(:domain => 'FOO')
943
- #
944
- # # Get a specific group on a domain
945
- # Sys::Admin.groups(:name => 'Some Group', :domain => 'FOO')
946
- #
947
- def self.groups(options = {})
948
- options = munge_options(options)
949
-
950
- host = options.delete(:host) || Socket.gethostname
951
- cs = "winmgmts:{impersonationLevel=impersonate}!"
952
- cs << "//#{host}/root/cimv2"
953
-
954
- begin
955
- wmi = WIN32OLE.connect(cs)
956
- rescue WIN32OLERuntimeError => err
957
- raise Error, err
958
- end
959
-
960
- query = "select * from win32_group"
961
-
962
- i = 0
963
-
964
- options.each{ |opt, val|
965
- if i == 0
966
- query << " where #{opt} = '#{val}'"
967
- i += 1
968
- else
969
- query << " and #{opt} = '#{val}'"
970
- end
971
- }
972
-
973
- array = []
974
- domain = options[:domain] || host
975
-
976
- wmi.execquery(query).each{ |group|
977
- grp = Group.new do |g|
978
- g.caption = group.caption
979
- g.description = group.description
980
- g.domain = group.domain
981
- g.gid = group.sid.split("-").last.to_i
982
- g.install_date = group.installdate
983
- g.local = group.localaccount
984
- g.name = group.name
985
- g.sid = group.sid
986
- g.sid_type = group.sidtype
987
- g.status = group.status
988
- g.members = get_members(domain, group.name)
989
- end
990
-
991
- array.push(grp)
992
- }
993
-
994
- array
995
- end
996
- end
997
- end
1
+ require 'ffi'
2
+ require 'win32ole'
3
+ require 'win32/security'
4
+ require 'win32/registry'
5
+ require 'socket'
6
+
7
+ module Sys
8
+ class Group
9
+ # Short description of the object.
10
+ attr_accessor :caption
11
+
12
+ # Description of the group.
13
+ attr_accessor :description
14
+
15
+ # Name of the Windows domain to which the group account belongs.
16
+ attr_accessor :domain
17
+
18
+ # Date the group was added.
19
+ attr_accessor :install_date
20
+
21
+ # Name of the Windows group account on the Group#domain specified.
22
+ attr_accessor :name
23
+
24
+ # Security identifier for this group.
25
+ attr_accessor :sid
26
+
27
+ # Current status for the group, such as "ok", "error", etc.
28
+ attr_accessor :status
29
+
30
+ # The group ID.
31
+ attr_accessor :gid
32
+
33
+ # Sets whether or not the group is local (as opposed to global).
34
+ attr_writer :local
35
+
36
+ # An array of members for that group. May contain SID's.
37
+ attr_accessor :members
38
+
39
+ # Creates and returns a new Group object. This class encapsulates
40
+ # the information for a group account, whether it be global or local.
41
+ #
42
+ # Yields +self+ if a block is given.
43
+ #
44
+ def initialize
45
+ yield self if block_given?
46
+ end
47
+
48
+ # Returns whether or not the group is a local group.
49
+ #
50
+ def local?
51
+ @local
52
+ end
53
+
54
+ # Returns the type of SID (Security Identifier) as a stringified value.
55
+ #
56
+ def sid_type
57
+ @sid_type
58
+ end
59
+
60
+ # Sets the SID (Security Identifier) type to +stype+, which can be
61
+ # one of the following constant values:
62
+ #
63
+ # * Admin::SidTypeUser
64
+ # * Admin::SidTypeGroup
65
+ # * Admin::SidTypeDomain
66
+ # * Admin::SidTypeAlias
67
+ # * Admin::SidTypeWellKnownGroup
68
+ # * Admin::SidTypeDeletedAccount
69
+ # * Admin::SidTypeInvalid
70
+ # * Admin::SidTypeUnknown
71
+ # * Admin::SidTypeComputer
72
+ #
73
+ def sid_type=(stype)
74
+ if stype.kind_of?(String)
75
+ @sid_type = stype.downcase
76
+ else
77
+ case stype
78
+ when Admin::SidTypeUser
79
+ @sid_type = "user"
80
+ when Admin::SidTypeGroup
81
+ @sid_type = "group"
82
+ when Admin::SidTypeDomain
83
+ @sid_type = "domain"
84
+ when Admin::SidTypeAlias
85
+ @sid_type = "alias"
86
+ when Admin::SidTypeWellKnownGroup
87
+ @sid_type = "well_known_group"
88
+ when Admin::SidTypeDeletedAccount
89
+ @sid_type = "deleted_account"
90
+ when Admin::SidTypeInvalid
91
+ @sid_type = "invalid"
92
+ when Admin::SidTypeUnknown
93
+ @sid_type = "unknown"
94
+ when Admin::SidTypeComputer
95
+ @sid_type = "computer"
96
+ else
97
+ @sid_type = "unknown"
98
+ end
99
+ end
100
+
101
+ @sid_type
102
+ end
103
+ end
104
+
105
+ class User
106
+ # An account for users whose primary account is in another domain.
107
+ TEMP_DUPLICATE = 0x0100
108
+
109
+ # Default account type that represents a typical user.
110
+ NORMAL = 0x0200
111
+
112
+ # A permit to trust account for a domain that trusts other domains.
113
+ INTERDOMAIN_TRUST = 0x0800
114
+
115
+ # An account for a Windows NT/2000 workstation or server that is a
116
+ # member of this domain.
117
+ WORKSTATION_TRUST = 0x1000
118
+
119
+ # A computer account for a backup domain controller that is a member
120
+ # of this domain.
121
+ SERVER_TRUST = 0x2000
122
+
123
+ # Domain and username of the account.
124
+ attr_accessor :caption
125
+
126
+ # Description of the account.
127
+ attr_accessor :description
128
+
129
+ # Name of the Windows domain to which a user account belongs.
130
+ attr_accessor :domain
131
+
132
+ # The user's password.
133
+ attr_accessor :password
134
+
135
+ # Full name of a local user.
136
+ attr_accessor :full_name
137
+
138
+ # An array of groups to which the user belongs.
139
+ attr_accessor :groups
140
+
141
+ # Date the user account was created.
142
+ attr_accessor :install_date
143
+
144
+ # Name of the Windows user account on the domain that the User#domain
145
+ # property specifies.
146
+ attr_accessor :name
147
+
148
+ # The user's security identifier.
149
+ attr_accessor :sid
150
+
151
+ # Current status for the user, such as "ok", "error", etc.
152
+ attr_accessor :status
153
+
154
+ # The user's id (RID).
155
+ attr_accessor :uid
156
+
157
+ # The user's primary group ID.
158
+ attr_accessor :gid
159
+
160
+ # The user's home directory
161
+ attr_accessor :dir
162
+
163
+ # Used to set whether or not the account is disabled.
164
+ attr_writer :disabled
165
+
166
+ # Sets whether or not the account is defined on the local computer.
167
+ attr_writer :local
168
+
169
+ # Sets whether or not the account is locked out of the OS.
170
+ attr_writer :lockout
171
+
172
+ # Sets whether or not the password for the account can be changed.
173
+ attr_writer :password_changeable
174
+
175
+ # Sets whether or not the password for the account expires.
176
+ attr_writer :password_expires
177
+
178
+ # Sets whether or not a password is required for the account.
179
+ attr_writer :password_required
180
+
181
+ # Returns the account type as a human readable string.
182
+ attr_reader :account_type
183
+
184
+ # Creates an returns a new User object. A User object encapsulates a
185
+ # user account on the operating system.
186
+ #
187
+ # Yields +self+ if a block is provided.
188
+ #
189
+ def initialize
190
+ yield self if block_given?
191
+ end
192
+
193
+ # Sets the account type for the account. Possible values are:
194
+ #
195
+ # * User::TEMP_DUPLICATE
196
+ # * User::NORMAL
197
+ # * User::INTERDOMAIN_TRUST
198
+ # * User::WORKSTATION_TRUST
199
+ # * User::SERVER_TRUST
200
+ #
201
+ def account_type=(type)
202
+ case type
203
+ when TEMP_DUPLICATE
204
+ @account_type = 'duplicate'
205
+ when NORMAL
206
+ @account_type = 'normal'
207
+ when INTERDOMAIN_TRUST
208
+ @account_type = 'interdomain_trust'
209
+ when WORKSTATION_TRUST
210
+ @account_type = 'workstation_trust'
211
+ when SERVER_TRUST
212
+ @account_type = 'server_trust'
213
+ else
214
+ @account_type = 'unknown'
215
+ end
216
+ end
217
+
218
+ # Returns the SID type as a human readable string.
219
+ #
220
+ def sid_type
221
+ @sid_type
222
+ end
223
+
224
+ # Sets the SID (Security Identifier) type to +stype+, which can be
225
+ # one of the following constant values:
226
+ #
227
+ # * Admin::SidTypeUser
228
+ # * Admin::SidTypeGroup
229
+ # * Admin::SidTypeDomain
230
+ # * Admin::SidTypeAlias
231
+ # * Admin::SidTypeWellKnownGroup
232
+ # * Admin::SidTypeDeletedAccount
233
+ # * Admin::SidTypeInvalid
234
+ # * Admin::SidTypeUnknown
235
+ # * Admin::SidTypeComputer
236
+ #
237
+ def sid_type=(stype)
238
+ case stype
239
+ when Admin::SidTypeUser
240
+ @sid_type = 'user'
241
+ when Admin::SidTypeGroup
242
+ @sid_type = 'group'
243
+ when Admin::SidTypeDomain
244
+ @sid_type = 'domain'
245
+ when Admin::SidTypeAlias
246
+ @sid_type = 'alias'
247
+ when Admin::SidTypeWellKnownGroup
248
+ @sid_type = 'well_known_group'
249
+ when Admin::SidTypeDeletedAccount
250
+ @sid_type = 'deleted_account'
251
+ when Admin::SidTypeInvalid
252
+ @sid_type = 'invalid'
253
+ when Admin::SidTypeUnknown
254
+ @sid_type = 'unknown'
255
+ when Admin::SidTypeComputer
256
+ @sid_type = 'computer'
257
+ else
258
+ @sid_type = 'unknown'
259
+ end
260
+ end
261
+
262
+ # Returns whether or not the account is disabled.
263
+ #
264
+ def disabled?
265
+ @disabled
266
+ end
267
+
268
+ # Returns whether or not the account is local.
269
+ #
270
+ def local?
271
+ @local
272
+ end
273
+
274
+ # Returns whether or not the account is locked out.
275
+ #
276
+ def lockout?
277
+ @lockout
278
+ end
279
+
280
+ # Returns whether or not the password for the account is changeable.
281
+ #
282
+ def password_changeable?
283
+ @password_changeable
284
+ end
285
+
286
+ # Returns whether or not the password for the account is changeable.
287
+ #
288
+ def password_expires?
289
+ @password_expires
290
+ end
291
+
292
+ # Returns whether or not the a password is required for the account.
293
+ #
294
+ def password_required?
295
+ @password_required
296
+ end
297
+ end
298
+
299
+ class Admin
300
+ extend FFI::Library
301
+
302
+ # This is the error raised in the majority of cases if anything goes wrong
303
+ # with any of the Sys::Admin methods.
304
+ #
305
+ class Error < StandardError; end
306
+
307
+ SidTypeUser = 1
308
+ SidTypeGroup = 2
309
+ SidTypeDomain = 3
310
+ SidTypeAlias = 4
311
+ SidTypeWellKnownGroup = 5
312
+ SidTypeDeletedAccount = 6
313
+ SidTypeInvalid = 7
314
+ SidTypeUnknown = 8
315
+ SidTypeComputer = 9
316
+
317
+ private
318
+
319
+ HKEY = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\"
320
+
321
+ # Retrieves the user's home directory. For local accounts query the
322
+ # registry. For domain accounts use ADSI and use the HomeDirectory.
323
+ #
324
+ def self.get_home_dir(user, local = false, domain = nil)
325
+ if local
326
+ sec = Win32::Security::SID.open(user)
327
+ key = HKEY + sec.to_s
328
+ dir = nil
329
+
330
+ begin
331
+ Win32::Registry::HKEY_LOCAL_MACHINE.open(key) do |reg|
332
+ dir = reg['ProfileImagePath']
333
+ end
334
+ rescue Win32::Registry::Error
335
+ # Not every local user has a home directory
336
+ end
337
+ else
338
+ domain ||= Socket.gethostname
339
+ adsi = WIN32OLE.connect("WinNT://#{domain}/#{user},user")
340
+ dir = adsi.get('HomeDirectory')
341
+ end
342
+
343
+ dir
344
+ end
345
+
346
+ # A private method that lower cases all keys, and converts them
347
+ # all to symbols.
348
+ #
349
+ def self.munge_options(opts)
350
+ rhash = {}
351
+
352
+ opts.each{ |k, v|
353
+ k = k.to_s.downcase.to_sym
354
+ rhash[k] = v
355
+ }
356
+
357
+ rhash
358
+ end
359
+
360
+ # An internal, private method for getting a list of groups for
361
+ # a particular user. The first member is a list of group names,
362
+ # the second member is the primary group ID.
363
+ #
364
+ def self.get_groups(domain, user)
365
+ array = []
366
+ adsi = WIN32OLE.connect("WinNT://#{domain}/#{user},user")
367
+ adsi.groups.each{ |g| array << g.name }
368
+ [array, adsi.PrimaryGroupId]
369
+ end
370
+
371
+ # An internal, private method for getting a list of members for
372
+ # any particular group.
373
+ #
374
+ def self.get_members(domain, group)
375
+ array = []
376
+ adsi = WIN32OLE.connect("WinNT://#{domain}/#{group}")
377
+ adsi.members.each{ |g| array << g.name }
378
+ array
379
+ end
380
+
381
+ # Used by the get_login method
382
+ ffi_lib :advapi32
383
+ attach_function :GetUserNameW, [:pointer, :pointer], :bool
384
+ private_class_method :GetUserNameW
385
+
386
+ public
387
+
388
+ # Creates the given +user+. If no domain option is specified,
389
+ # then it defaults to your local host, i.e. a local account is
390
+ # created.
391
+ #
392
+ # Any options provided are treated as IADsUser interface methods
393
+ # and are called before SetInfo is finally called.
394
+ #
395
+ # Examples:
396
+ #
397
+ # # Create a local user with no options
398
+ # Sys::Admin.add_user(:name => 'asmith')
399
+ #
400
+ # # Create a local user with options
401
+ # Sys::Admin.add_user(
402
+ # :name => 'asmith',
403
+ # :description => 'Really cool guy',
404
+ # :password => 'abc123'
405
+ # )
406
+ #
407
+ # # Create a user on a specific domain
408
+ # Sys::Admin.add_user(
409
+ # :name => 'asmith',
410
+ # :domain => 'XX',
411
+ # :fullname => 'Al Smith'
412
+ # )
413
+ #--
414
+ # Most options are passed to the 'put' method. However, we handle the
415
+ # password specially since it's a separate method, and some environments
416
+ # require that it be set up front.
417
+ #
418
+ def self.add_user(options = {})
419
+ options = munge_options(options)
420
+
421
+ name = options.delete(:name) or raise ArgumentError, 'No user given'
422
+ domain = options[:domain]
423
+
424
+ if domain.nil?
425
+ domain = Socket.gethostname
426
+ moniker = "WinNT://#{domain},Computer"
427
+ else
428
+ moniker = "WinNT://#{domain}"
429
+ end
430
+
431
+ begin
432
+ adsi = WIN32OLE.connect(moniker)
433
+ user = adsi.create('user', name)
434
+
435
+ options.each{ |option, value|
436
+ if option.to_s == 'password'
437
+ user.setpassword(value)
438
+ else
439
+ user.put(option.to_s, value)
440
+ end
441
+ }
442
+
443
+ user.setinfo
444
+ rescue WIN32OLERuntimeError => err
445
+ raise Error, err
446
+ end
447
+ end
448
+
449
+ # Configures the +user+ using +options+. If no domain option is
450
+ # specified then your local host is used, i.e. you are configuring
451
+ # a local user account.
452
+ #
453
+ # See http://tinyurl.com/3hjv9 for a list of valid options.
454
+ #
455
+ # In the case of a password change, pass a two element array as the
456
+ # old value and new value.
457
+ #
458
+ # Examples:
459
+ #
460
+ # # Configure a local user
461
+ # Sys::Admin.configure_user(
462
+ # :name => 'djberge',
463
+ # :description => 'Awesome'
464
+ # )
465
+ #
466
+ # # Change the password
467
+ # Sys::Admin.configure_user(
468
+ # :name => 'asmith',
469
+ # :password => 'new_password'
470
+ # )
471
+ #
472
+ # # Configure a user on a specific domain
473
+ # Sys::Admin.configure_user(
474
+ # :name => 'jsmrz',
475
+ # :domain => 'XX',
476
+ # :firstname => 'Jo'
477
+ # )
478
+ #
479
+ def self.configure_user(options = {})
480
+ options = munge_options(options)
481
+
482
+ name = options.delete(:name) or raise ArgumentError, 'No name given'
483
+ domain = options[:domain] || Socket.gethostname
484
+
485
+ begin
486
+ adsi = WIN32OLE.connect("WinNT://#{domain}/#{name},user")
487
+
488
+ options.each{ |option, value|
489
+ if option.to_s == 'password'
490
+ adsi.setpassword(value)
491
+ else
492
+ adsi.put(option.to_s, value)
493
+ end
494
+ }
495
+
496
+ adsi.setinfo
497
+ rescue WIN32OLERuntimeError => err
498
+ raise Error, err
499
+ end
500
+ end
501
+
502
+ # Deletes the given +user+ on +domain+. If no domain is specified,
503
+ # then it defaults to your local host, i.e. a local account is
504
+ # deleted.
505
+ #
506
+ def self.delete_user(user, domain = nil)
507
+ if domain.nil?
508
+ domain = Socket.gethostname
509
+ moniker = "WinNT://#{domain},Computer"
510
+ else
511
+ moniker = "WinNT://#{domain}"
512
+ end
513
+
514
+ begin
515
+ adsi = WIN32OLE.connect(moniker)
516
+ adsi.delete('user', user)
517
+ rescue WIN32OLERuntimeError => err
518
+ raise Error, err
519
+ end
520
+ end
521
+
522
+ # Create a new +group+ using +options+. If no domain option is specified
523
+ # then a local group is created instead.
524
+ #
525
+ # Examples:
526
+ #
527
+ # # Create a local group with no options
528
+ # Sys::Admin.add_group(:name => 'Dudes')
529
+ #
530
+ # # Create a local group with options
531
+ # Sys::Admin.add_group(:name => 'Dudes', :description => 'Boys')
532
+ #
533
+ # # Create a group on a specific domain
534
+ # Sys::Admin.add_group(
535
+ # :name => 'Ladies',
536
+ # :domain => 'XYZ',
537
+ # :description => 'Girls'
538
+ # )
539
+ #
540
+ def self.add_group(options = {})
541
+ options = munge_options(options)
542
+
543
+ group = options.delete(:name) or raise ArgumentError, 'No name given'
544
+ domain = options[:domain]
545
+
546
+ if domain.nil?
547
+ domain = Socket.gethostname
548
+ moniker = "WinNT://#{domain},Computer"
549
+ else
550
+ moniker = "WinNT://#{domain}"
551
+ end
552
+
553
+ begin
554
+ adsi = WIN32OLE.connect(moniker)
555
+ group = adsi.create('group', group)
556
+ group.setinfo
557
+ configure_group(options) unless options.empty?
558
+ rescue WIN32OLERuntimeError => err
559
+ raise Error, err
560
+ end
561
+ end
562
+
563
+ # Adds +user+ to +group+ on the specified +domain+, or the localhost
564
+ # if no domain is specified.
565
+ #
566
+ def self.add_group_member(user, group, domain=nil)
567
+ domain ||= Socket.gethostname
568
+ adsi = WIN32OLE.connect("WinNT://#{domain}/#{group},group")
569
+ adsi.Add("WinNT://#{domain}/#{user}")
570
+ rescue WIN32OLERuntimeError => err
571
+ raise Error, err
572
+ end
573
+
574
+ # Removes +user+ from +group+ on the specified +domain+, or the localhost
575
+ # if no domain is specified.
576
+ #
577
+ def self.remove_group_member(user, group, domain=nil)
578
+ domain ||= Socket.gethostname
579
+ adsi = WIN32OLE.connect("WinNT://#{domain}/#{group},group")
580
+ adsi.Remove("WinNT://#{domain}/#{user}")
581
+ rescue WIN32OLERuntimeError => err
582
+ raise Error, err
583
+ end
584
+
585
+ # Configures the +group+ using +options+. If no domain option is
586
+ # specified then your local host is used, i.e. you are configuring
587
+ # a local group.
588
+ #
589
+ # See http://tinyurl.com/cjkzl for a list of valid options.
590
+ #
591
+ # Examples:
592
+ #
593
+ # # Configure a local group.
594
+ # Sys::Admin.configure_group(:name => 'Abba', :description => 'Swedish')
595
+ #
596
+ # # Configure a group on a specific domain.
597
+ # Sys::Admin.configure_group(
598
+ # :name => 'Web Team',
599
+ # :domain => 'Foo',
600
+ # :description => 'Web programming cowboys'
601
+ # )
602
+ #
603
+ def self.configure_group(options = {})
604
+ options = munge_options(options)
605
+
606
+ group = options.delete(:name) or raise ArgumentError, 'No name given'
607
+ domain = options[:domain] || Socket.gethostname
608
+
609
+ begin
610
+ adsi = WIN32OLE.connect("WinNT://#{domain}/#{group},group")
611
+ options.each{ |option, value| adsi.put(option.to_s, value) }
612
+ adsi.setinfo
613
+ rescue WIN32OLERuntimeError => err
614
+ raise Error, err
615
+ end
616
+ end
617
+
618
+ # Delete the +group+ from +domain+. If no domain is specified, then
619
+ # you are deleting a local group.
620
+ #
621
+ def self.delete_group(group, domain = nil)
622
+ if domain.nil?
623
+ domain = Socket.gethostname
624
+ moniker = "WinNT://#{domain},Computer"
625
+ else
626
+ moniker = "WinNT://#{domain}"
627
+ end
628
+
629
+ begin
630
+ adsi = WIN32OLE.connect(moniker)
631
+ adsi.delete('group', group)
632
+ rescue WIN32OLERuntimeError => err
633
+ raise Error, err
634
+ end
635
+ end
636
+
637
+ # Returns the user name (only) of the current login.
638
+ #
639
+ def self.get_login
640
+ buffer = FFI::MemoryPointer.new(:char, 256)
641
+ nsize = FFI::MemoryPointer.new(:ulong)
642
+ nsize.write_ulong(buffer.size)
643
+
644
+ unless GetUserNameW(buffer, nsize)
645
+ raise Error, 'GetUserName() call failed in get_login'
646
+ end
647
+
648
+ buffer.read_string(nsize.read_ulong * 2).tr(0.chr, '')
649
+ end
650
+
651
+ # Returns a User object based on either +name+ or +uid+.
652
+ #
653
+ # call-seq:
654
+ # Sys::Admin.get_user(name, options = {})
655
+ # Sys::Admin.get_user(uid, options = {})
656
+ #
657
+ # Looks for +usr+ information based on the options you specify, where
658
+ # the +usr+ argument can be either a user name or a RID.
659
+ #
660
+ # If a 'host' option is specified, information is retrieved from that
661
+ # host. Otherwise, the local host is used.
662
+ #
663
+ # All other options are converted to WQL statements against the
664
+ # Win32_UserAccount WMI object. See http://tinyurl.com/by9nvn for a
665
+ # list of possible options.
666
+ #
667
+ # Examples:
668
+ #
669
+ # # Get a user by name
670
+ # Admin.get_user('djberge')
671
+ #
672
+ # # Get a user by uid
673
+ # Admin.get_user(100)
674
+ #
675
+ # # Get a user on a specific domain
676
+ # Admin.get_user('djberge', :domain => 'TEST')
677
+ #--
678
+ # The reason for keeping the +usr+ variable as a separate argument
679
+ # instead of rolling it into the options hash was to keep a unified
680
+ # API between the Windows and UNIX versions.
681
+ #
682
+ def self.get_user(usr, options = {})
683
+ options = munge_options(options)
684
+
685
+ host = options.delete(:host) || Socket.gethostname
686
+ cs = "winmgmts:{impersonationLevel=impersonate}!"
687
+ cs << "//#{host}/root/cimv2"
688
+
689
+ begin
690
+ wmi = WIN32OLE.connect(cs)
691
+ rescue WIN32OLERuntimeError => err
692
+ raise Error, err
693
+ end
694
+
695
+ query = "select * from win32_useraccount"
696
+
697
+ i = 0
698
+
699
+ options.each{ |opt, val|
700
+ if i == 0
701
+ query << " where #{opt} = '#{val}'"
702
+ i += 1
703
+ else
704
+ query << " and #{opt} = '#{val}'"
705
+ end
706
+ }
707
+
708
+ if usr.kind_of?(Fixnum)
709
+ query << " and sid like '%-#{usr}'"
710
+ else
711
+ query << " and name = '#{usr}'"
712
+ end
713
+
714
+ domain = options[:domain] || host
715
+
716
+ wmi.execquery(query).each{ |user|
717
+ uid = user.sid.split('-').last.to_i
718
+
719
+ # Because our 'like' query isn't fulproof, let's parse
720
+ # the SID again to make sure
721
+ if usr.kind_of?(Fixnum)
722
+ next if usr != uid
723
+ end
724
+
725
+ groups, primary_group = *get_groups(domain, user.name)
726
+
727
+ user_object = User.new do |u|
728
+ u.account_type = user.accounttype
729
+ u.caption = user.caption
730
+ u.description = user.description
731
+ u.disabled = user.disabled
732
+ u.domain = user.domain
733
+ u.full_name = user.fullname
734
+ u.install_date = user.installdate
735
+ u.local = user.localaccount
736
+ u.lockout = user.lockout
737
+ u.name = user.name
738
+ u.password_changeable = user.passwordchangeable
739
+ u.password_expires = user.passwordexpires
740
+ u.password_required = user.passwordrequired
741
+ u.sid = user.sid
742
+ u.sid_type = user.sidtype
743
+ u.status = user.status
744
+ u.uid = uid
745
+ u.gid = primary_group
746
+ u.groups = groups
747
+ u.dir = get_home_dir(user.name, options[:localaccount], domain)
748
+ end
749
+
750
+ return user_object
751
+ }
752
+
753
+ # If we're here, it means it wasn't found.
754
+ raise Error, "no user found for '#{usr}'"
755
+ end
756
+
757
+ # Returns an array of User objects for each user on the system.
758
+ #
759
+ # You may specify a host from which information is retrieved. The
760
+ # default is the local host.
761
+ #
762
+ # All other arguments are passed as WQL query parameters against
763
+ # the Win32_UserAccont WMI object.
764
+ #
765
+ # Examples:
766
+ #
767
+ # # Get all local account users
768
+ # Sys::Admin.users(:localaccount => true)
769
+ #
770
+ # # Get all user accounts on a specific domain
771
+ # Sys::Admin.users(:domain => 'FOO')
772
+ #
773
+ # # Get a single user from a domain
774
+ # Sys::Admin.users(:name => 'djberge', :domain => 'FOO')
775
+ #
776
+ def self.users(options = {})
777
+ options = munge_options(options)
778
+
779
+ host = options.delete(:host) || Socket.gethostname
780
+ cs = "winmgmts:{impersonationLevel=impersonate}!"
781
+ cs << "//#{host}/root/cimv2"
782
+
783
+ begin
784
+ wmi = WIN32OLE.connect(cs)
785
+ rescue WIN32OLERuntimeError => e
786
+ raise Error, e
787
+ end
788
+
789
+ query = "select * from win32_useraccount"
790
+
791
+ i = 0
792
+
793
+ options.each{ |opt, val|
794
+ if i == 0
795
+ query << " where #{opt} = '#{val}'"
796
+ i += 1
797
+ else
798
+ query << " and #{opt} = '#{val}'"
799
+ end
800
+ }
801
+
802
+ array = []
803
+ domain = options[:domain] || host
804
+
805
+ wmi.execquery(query).each{ |user|
806
+ uid = user.sid.split('-').last.to_i
807
+
808
+ usr = User.new do |u|
809
+ u.account_type = user.accounttype
810
+ u.caption = user.caption
811
+ u.description = user.description
812
+ u.disabled = user.disabled
813
+ u.domain = user.domain
814
+ u.full_name = user.fullname
815
+ u.install_date = user.installdate
816
+ u.local = user.localaccount
817
+ u.lockout = user.lockout
818
+ u.name = user.name
819
+ u.password_changeable = user.passwordchangeable
820
+ u.password_expires = user.passwordexpires
821
+ u.password_required = user.passwordrequired
822
+ u.sid = user.sid
823
+ u.sid_type = user.sidtype
824
+ u.status = user.status
825
+ u.groups = get_groups(domain, user.name)
826
+ u.uid = uid
827
+ u.dir = get_home_dir(user.name, options[:localaccount], host)
828
+ end
829
+
830
+ array.push(usr)
831
+ }
832
+
833
+ array
834
+ end
835
+
836
+ # Returns a Group object based on either +name+ or +gid+.
837
+ #
838
+ # call-seq:
839
+ # Sys::Admin.get_group(name, options = {})
840
+ # Sys::Admin.get_group(gid, options = {})
841
+ #
842
+ # If a numeric value is sent as the first parameter, it is treated
843
+ # as a RID and is checked against the SID for a match.
844
+ #
845
+ # You may specify a host as an option from which information is
846
+ # retrieved. The default is the local host.
847
+ #
848
+ # All other options are passed as WQL parameters to the Win32_Group
849
+ # WMI object. See http://tinyurl.com/bngc8s for a list of possible
850
+ # options.
851
+ #
852
+ # Examples:
853
+ #
854
+ # # Find a group by name
855
+ # Sys::Admin.get_group('Web Team')
856
+ #
857
+ # # Find a group by id
858
+ # Sys::Admin.get_group(31667)
859
+ #
860
+ # # Find a group on a specific domain
861
+ # Sys::Admin.get_group('Web Team', :domain => 'FOO')
862
+ #
863
+ def self.get_group(grp, options = {})
864
+ options = munge_options(options)
865
+
866
+ host = options.delete(:host) || Socket.gethostname
867
+ cs = "winmgmts:{impersonationLevel=impersonate}!"
868
+ cs << "//#{host}/root/cimv2"
869
+
870
+ begin
871
+ wmi = WIN32OLE.connect(cs)
872
+ rescue WIN32OLERuntimeError => err
873
+ raise Error, err
874
+ end
875
+
876
+ query = "select * from win32_group"
877
+
878
+ i = 0
879
+
880
+ options.each{ |opt, val|
881
+ if i == 0
882
+ query << " where #{opt} = '#{val}'"
883
+ i += 1
884
+ else
885
+ query << " and #{opt} = '#{val}'"
886
+ end
887
+ }
888
+
889
+ if grp.kind_of?(Fixnum)
890
+ query << " and sid like '%-#{grp}'"
891
+ else
892
+ query << " and name = '#{grp}'"
893
+ end
894
+
895
+ domain = options[:domain] || host
896
+
897
+ wmi.execquery(query).each{ |group|
898
+ gid = group.sid.split("-").last.to_i
899
+
900
+ # Because our 'like' query isn't fulproof, let's parse
901
+ # the SID again to make sure
902
+ if grp.kind_of?(Fixnum)
903
+ next if grp != gid
904
+ end
905
+
906
+ group_object = Group.new do |g|
907
+ g.caption = group.caption
908
+ g.description = group.description
909
+ g.domain = group.domain
910
+ g.gid = gid
911
+ g.install_date = group.installdate
912
+ g.local = group.localaccount
913
+ g.name = group.name
914
+ g.sid = group.sid
915
+ g.sid_type = group.sidtype
916
+ g.status = group.status
917
+ g.members = get_members(domain, group.name)
918
+ end
919
+
920
+ return group_object
921
+ }
922
+
923
+ # If we're here, it means it wasn't found.
924
+ raise Error, "no group found for '#{grp}'"
925
+ end
926
+
927
+ # Returns an array of Group objects for each user on the system.
928
+ #
929
+ # You may specify a host option from which information is retrieved.
930
+ # The default is the local host.
931
+ #
932
+ # All other options are passed as WQL parameters to the Win32_Group
933
+ # WMI object. See http://tinyurl.com/bngc8s for a list of possible
934
+ # options.
935
+ #
936
+ # Examples:
937
+ #
938
+ # # Get local group information
939
+ # Sys::Admin.groups(:localaccount => true)
940
+ #
941
+ # # Get all groups on a specific domain
942
+ # Sys::Admin.groups(:domain => 'FOO')
943
+ #
944
+ # # Get a specific group on a domain
945
+ # Sys::Admin.groups(:name => 'Some Group', :domain => 'FOO')
946
+ #
947
+ def self.groups(options = {})
948
+ options = munge_options(options)
949
+
950
+ host = options.delete(:host) || Socket.gethostname
951
+ cs = "winmgmts:{impersonationLevel=impersonate}!"
952
+ cs << "//#{host}/root/cimv2"
953
+
954
+ begin
955
+ wmi = WIN32OLE.connect(cs)
956
+ rescue WIN32OLERuntimeError => err
957
+ raise Error, err
958
+ end
959
+
960
+ query = "select * from win32_group"
961
+
962
+ i = 0
963
+
964
+ options.each{ |opt, val|
965
+ if i == 0
966
+ query << " where #{opt} = '#{val}'"
967
+ i += 1
968
+ else
969
+ query << " and #{opt} = '#{val}'"
970
+ end
971
+ }
972
+
973
+ array = []
974
+ domain = options[:domain] || host
975
+
976
+ wmi.execquery(query).each{ |group|
977
+ grp = Group.new do |g|
978
+ g.caption = group.caption
979
+ g.description = group.description
980
+ g.domain = group.domain
981
+ g.gid = group.sid.split("-").last.to_i
982
+ g.install_date = group.installdate
983
+ g.local = group.localaccount
984
+ g.name = group.name
985
+ g.sid = group.sid
986
+ g.sid_type = group.sidtype
987
+ g.status = group.status
988
+ g.members = get_members(domain, group.name)
989
+ end
990
+
991
+ array.push(grp)
992
+ }
993
+
994
+ array
995
+ end
996
+ end
997
+ end