require "win32ole" require "Win32API" require "socket" module Sys # This is the error raised in the majority of cases if anything goes wrong # with any of the Sys::Admin methods. # class AdminError < StandardError; end class Group # Short description of the object. attr_accessor :caption # Description of the group. attr_accessor :description # Name of the Windows domain to which the group account belongs. attr_accessor :domain # Date the group was added. attr_accessor :install_date # Name of the Windows group account on the Group#domain specified. attr_accessor :name # Security identifier for this group. attr_accessor :sid # Current status for the group, such as "ok", "error", etc. attr_accessor :status # The group ID. attr_accessor :gid # Sets whether or not the group is local (as opposed to global). attr_writer :local # Creates and returns a new Group object. This class encapsulates # the information for a group account, whether it be global or local. # # Yields +self+ if a block is given. # def initialize yield self if block_given? end # Returns whether or not the group is a local group. # def local? @local end # Returns the type of SID (Security Identifier) as a stringified value. # def sid_type @sid_type end # Sets the SID (Security Identifier) type to +stype+, which can be # one of the following constant values: # # * Admin::SidTypeUser # * Admin::SidTypeGroup # * Admin::SidTypeDomain # * Admin::SidTypeAlias # * Admin::SidTypeWellKnownGroup # * Admin::SidTypeDeletedAccount # * Admin::SidTypeInvalid # * Admin::SidTypeUnknown # * Admin::SidTypeComputer # def sid_type=(stype) if stype.kind_of?(String) @sid_type = stype.downcase else case stype when Admin::SidTypeUser @sid_type = "user" when Admin::SidTypeGroup @sid_type = "group" when Admin::SidTypeDomain @sid_type = "domain" when Admin::SidTypeAlias @sid_type = "alias" when Admin::SidTypeWellKnownGroup @sid_type = "well_known_group" when Admin::SidTypeDeletedAccount @sid_type = "deleted_account" when Admin::SidTypeInvalid @sid_type = "invalid" when Admin::SidTypeUnknown @sid_type = "unknown" when Admin::SidTypeComputer @sid_type = "computer" else @sid_type = "unknown" end end @sid_type end end class User # An account for users whose primary account is in another domain. TEMP_DUPLICATE = 0x0100 # Default account type that represents a typical user. NORMAL = 0x0200 # A permit to trust account for a domain that trusts other domains. INTERDOMAIN_TRUST = 0x0800 # An account for a Windows NT/2000 workstation or server that is a # member of this domain. WORKSTATION_TRUST = 0x1000 # A computer account for a backup domain controller that is a member # of this domain. SERVER_TRUST = 0x2000 # Domain and username of the account. attr_accessor :caption # Description of the account. attr_accessor :description # Name of the Windows domain to which a user account belongs. attr_accessor :domain # The user's password. attr_accessor :password # Full name of a local user. attr_accessor :full_name # Date the user account was created. attr_accessor :install_date # Name of the Windows user account on the domain that the User#domain # property specifies. attr_accessor :name # The user's security identifier. attr_accessor :sid # Current status for the group, such as "ok", "error", etc. attr_accessor :status # Used to set whether or not the account is disabled. attr_writer :disabled # Sets whether or not the account is defined on the local computer. attr_writer :local # Sets whether or not the account is locked out of the OS. attr_writer :lockout # Sets whether or not the password for the account can be changed. attr_writer :password_changeable # Sets whether or not the password for the account expires. attr_writer :password_expires # Sets whether or not a password is required for the account. attr_writer :password_required # Returns the account type as a human readable string. attr_reader :account_type # Creates an returns a new User object. A User object encapsulates a # user account on the operating system. # # Yields +self+ if a block is provided. # def initialize yield self if block_given? end # Sets the account type for the account. Possible values are: # # * User::TEMP_DUPLICATE # * User::NORMAL # * User::INTERDOMAIN_TRUST # * User::WORKSTATION_TRUST # * User::SERVER_TRUST # def account_type=(type) case type when TEMP_DUPLICATE @account_type = "duplicate" when NORMAL @account_type = "normal" when INTERDOMAIN_TRUST @account_type = "interdomain_trust" when WORKSTATION_TRUST @account_type = "workstation_trust" when SERVER_TRUST @account_type = "server_trust" else @account_type = "unknown" end end # Returns the SID type as a human readable string. # def sid_type @sid_type end # Sets the SID (Security Identifier) type to +stype+, which can be # one of the following constant values: # # * Admin::SidTypeUser # * Admin::SidTypeGroup # * Admin::SidTypeDomain # * Admin::SidTypeAlias # * Admin::SidTypeWellKnownGroup # * Admin::SidTypeDeletedAccount # * Admin::SidTypeInvalid # * Admin::SidTypeUnknown # * Admin::SidTypeComputer # def sid_type=(stype) case stype when Admin::SidTypeUser @sid_type = "user" when Admin::SidTypeGroup @sid_type = "group" when Admin::SidTypeDomain @sid_type = "domain" when Admin::SidTypeAlias @sid_type = "alias" when Admin::SidTypeWellKnownGroup @sid_type = "well_known_group" when Admin::SidTypeDeletedAccount @sid_type = "deleted_account" when Admin::SidTypeInvalid @sid_type = "invalid" when Admin::SidTypeUnknown @sid_type = "unknown" when Admin::SidTypeComputer @sid_type = "computer" else @sid_type = "unknown" end end # Returns whether or not the account is disabled. # def disabled? @disabled end # Returns whether or not the account is local. # def local? @local end # Returns whether or not the account is locked out. # def lockout? @lockout end # Returns whether or not the password for the account is changeable. # def password_changeable? @password_changeable end # Returns whether or not the password for the account is changeable. # def password_expires? @password_expires end # Returns whether or not the a password is required for the account. # def password_required? @password_required end end class Admin VERSION = '1.4.1' SidTypeUser = 1 SidTypeGroup = 2 SidTypeDomain = 3 SidTypeAlias = 4 SidTypeWellKnownGroup = 5 SidTypeDeletedAccount = 6 SidTypeInvalid = 7 SidTypeUnknown = 8 SidTypeComputer = 9 # Used by the get_login method GetUserName = Win32API.new('advapi32', 'GetUserName', 'PP', 'L') # :nodoc: # Configures the global +user+ on +domain+ using options. # # See http://tinyurl.com/3hjv9 for a list of valid options. # def self.config_global_user(user, options, domain) begin adsi = WIN32OLE.connect("WinNT://#{domain}/#{user},user") options.each{ |option, value| adsi.put(option.to_s, value) } adsi.setinfo rescue WIN32OLERuntimeError => err raise AdminError, err end end # Configures the local +user+ on +host+ using +options+. If no host # is specified, the default is localhost. # # See http://tinyurl.com/3hjv9 for a list of valid options. # def self.config_local_user(user, options, host=Socket.gethostname) begin adsi = WIN32OLE.connect("WinNT://#{host}/#{user},user") options.each{ |option, value| adsi.put(option.to_s, value) } adsi.setinfo rescue WIN32OLERuntimeError => err raise AdminError, err end end # Adds the global +user+ on +domain+ # def self.add_global_user(user, domain) begin adsi = WIN32OLE.connect("WinNT://#{host},Computer") user = adsi.create("user", user) user.setinfo rescue WIN32OLERuntimeError => err raise AdminError, err end end # Adds the local +user+ on +host+, or the localhost if none is specified. # def self.add_local_user(user, host=Socket.gethostname) begin adsi = WIN32OLE.connect("WinNT://#{host},Computer") user = adsi.create("user", user) user.setinfo rescue WIN32OLERuntimeError => err raise AdminError, err end end # Deletes the local +user+ from +host+, or localhost if no host specified. # def self.delete_local_user(user, host=Socket.gethostname) begin adsi = WIN32OLE.connect("WinNT://#{host},Computer") adsi.delete("user", user) rescue WIN32OLERuntimeError => err raise AdminError, err end end # Deletes the global +user+ from +domain+. # def self.delete_global_user(user, domain) begin adsi = WIN32OLE.connect("WinNT://#{domain}") adsi.delete("user", user) rescue WIN32OLERuntimeError => err raise AdminError, err end end # Configures the global +group+ on +domain+ using +options+. # # See http://tinyurl.com/cjkzl for a list of valid options. # def self.config_global_group(group, options, domain) begin adsi = WIN32OLE.connect("WinNT://#{domain}/#{group},group") options.each{ |option, value| adsi.put(option.to_s, value) } adsi.setinfo rescue WIN32OLERuntimeError => err raise AdminError, err end end # Configures the local +group+ on +host+ using +options+. If no host # is specified, the default is localhost. # # See http://tinyurl.com/cjkzl for a list of valid options. # def self.config_local_group(group, options, host=Socket.gethostname) begin adsi = WIN32OLE.connect("WinNT://#{host}/#{group},group") options.each{ |option, value| adsi.put(option.to_s, value) } adsi.setinfo rescue WIN32OLERuntimeError => err raise AdminError, err end end # Add global +group+ to +domain+. # def self.add_global_group(group, domain) begin adsi = WIN32OLE.connect("WinNT://#{domain},Computer") obj = adsi.create("group", group) obj.setinfo rescue WIN32OLERuntimeError => err raise AdminError, err end end # Add local +group+ to +host+, or the localhost if no host is specified. # def self.add_local_group(group, host=Socket.gethostname) begin adsi = WIN32OLE.connect("WinNT://#{host},Computer") obj = adsi.create("group", group) obj.setinfo rescue WIN32OLERuntimeError => err raise AdminError, err end end # Delete the global +group+ from +domain+. # def self.delete_global_group(groupid, domain) begin adsi = WIN32OLE.connect("WinNT://#{domain},Computer") obj = adsi.delete("group", groupid) rescue WIN32OLERuntimeError => err raise AdminError, err end end # Delete the local +group+ from +host+, or localhost if no host specified. # def self.delete_local_group(groupid, host=Socket.gethostname) begin adsi = WIN32OLE.connect("WinNT://#{host},Computer") obj = adsi.delete("group", groupid) rescue WIN32OLERuntimeError => err raise AdminError, err end end # Returns the user name (only) of the current login. # def self.get_login buffer = 0.chr * 256 nsize = [buffer.size].pack("L") if GetUserName.call(buffer, nsize) == 0 raise AdminError, 'GetUserName() call failed in get_login' end length = nsize.unpack("L")[0] username = buffer[0 ... length].chop username end # Returns a User object based on either +name+ or +uid+. # # call-seq: # get_user(name, host=localhost) # get_user(uid, host=localhost, local=true) # # You may specify a +host+ from which information is retrieved. The # default is the local machine. You may also specify whether to # retrieve a local or global account. The default is local. # def self.get_user(uid, host=Socket.gethostname, local=true) host = Socket.gethostname if host.nil? cs = "winmgmts:{impersonationLevel=impersonate}!" cs << "//#{host}/root/cimv2" begin wmi = WIN32OLE.connect(cs) rescue WIN32OLERuntimeError => e raise AdminError, e end query = "select * from win32_useraccount" query << " where localaccount = true" if local if uid.kind_of?(Fixnum) if local query << " and sid like '%-#{uid}'" else query << " where sid like '%-#{uid}'" end else if local query << " and name = '#{uid}'" else query << " where name = '#{uid}'" end end wmi.execquery(query).each{ |user| # Because our 'like' query isn't fulproof, let's parse # the SID again to make sure if uid.kind_of?(Fixnum) if user.sid.split("-").last.to_i != uid next end end usr = User.new do |u| u.account_type = user.accounttype u.caption = user.caption u.description = user.description u.disabled = user.disabled u.domain = user.domain u.full_name = user.fullname u.install_date = user.installdate u.local = user.localaccount u.lockout = user.lockout u.name = user.name u.password_changeable = user.passwordchangeable u.password_expires = user.passwordexpires u.password_required = user.passwordrequired u.sid = user.sid u.sid_type = user.sidtype u.status = user.status end return usr } end # In block form, yields a User object for each user on the system. In # non-block form, returns an Array of User objects. # # call-seq: # users(host=localhost, local=true) # users(host=localhost, local=true){ |user| ... } # # You may specify a host from which information is retrieved. The # default is the local machine. You can retrieve either a global or # local group, depending on the value of the +local+ argument. # def self.users(host=Socket.gethostname, local=true) host = Socket.gethostname if host.nil? cs = "winmgmts:{impersonationLevel=impersonate}!" cs << "//#{host}/root/cimv2" begin wmi = WIN32OLE.connect(cs) rescue WIN32OLERuntimeError => e raise AdminError, e end query = "select * from win32_useraccount" query << " where localaccount = true" if local array = [] wmi.execquery(query).each{ |user| usr = User.new do |u| u.account_type = user.accounttype u.caption = user.caption u.description = user.description u.disabled = user.disabled u.domain = user.domain u.full_name = user.fullname u.install_date = user.installdate u.local = user.localaccount u.lockout = user.lockout u.name = user.name u.password_changeable = user.passwordchangeable u.password_expires = user.passwordexpires u.password_required = user.passwordrequired u.sid = user.sid u.sid_type = user.sidtype u.status = user.status end if block_given? yield usr else array.push(usr) end } return array unless block_given? end # Returns a Group object based on either +name+ or +uid+. # # call-seq: # get_group(name, host=localhost, local=true) # get_group(gid, host=localhost, local=true) # # You may specify a host from which information is retrieved. # The default is the local machine. You can retrieve either a global or # local group, depending on the value of the +local+ argument. # def self.get_group(grp, host=Socket.gethostname, local=true) host = Socket.gethostname if host.nil? cs = "winmgmts:{impersonationLevel=impersonate}!" cs << "//#{host}/root/cimv2" gid = nil begin wmi = WIN32OLE.connect(cs) rescue WIN32OLERuntimeError => e raise AdminError, e end query = "select * from win32_group" query << " where localaccount = true" if local if grp.kind_of?(Fixnum) if local query << " and sid like '%-#{grp}'" else query << " where sid like '%-#{grp}'" end else if local query << " and name = '#{grp}'" else query << " where name = '#{grp}'" end end wmi.execquery(query).each{ |group| gid = group.sid.split("-").last.to_i # Because our 'like' query isn't fulproof, let's parse # the SID again to make sure if grp.kind_of?(Fixnum) next if grp != gid end grp = Group.new do |g| g.caption = group.caption g.description = group.description g.domain = group.domain g.gid = gid g.install_date = group.installdate g.local = group.localaccount g.name = group.name g.sid = group.sid g.sid_type = group.sidtype g.status = group.status end return grp } # If we're here, it means it wasn't found. raise AdminError, "no group found for '#{grp}'" end # In block form, yields a Group object for each user on the system. In # non-block form, returns an Array of Group objects. # # call-seq: # groups(host=localhost, local=true) # groups(host=localhost, local=true){ |group| ... } # # You may specify a host from which information is retrieved. # The default is the local machine. You can retrieve either a global or # local group, depending on the value of the +local+ argument. # def self.groups(host=Socket.gethostname, local=true) host = Socket.gethostname if host.nil? cs = "winmgmts:{impersonationLevel=impersonate}!" cs << "//#{host}/root/cimv2" begin wmi = WIN32OLE.connect(cs) rescue WIN32OLERuntimeError => e raise AdminError, e end query = "select * from win32_group" query << " where localaccount = true" if local array = [] wmi.execquery(query).each{ |group| grp = Group.new do |g| g.caption = group.caption g.description = group.description g.domain = group.domain g.gid = group.sid.split("-").last.to_i g.install_date = group.installdate g.local = group.localaccount g.name = group.name g.sid = group.sid g.sid_type = group.sidtype g.status = group.status end if block_given? yield grp else array.push(grp) end } return array unless block_given? end end end