diff --git a/lib/puppet/provider/group/windows_adsi.rb b/lib/puppet/provider/group/windows_adsi.rb index 4468d0071..6f88c68f7 100644 --- a/lib/puppet/provider/group/windows_adsi.rb +++ b/lib/puppet/provider/group/windows_adsi.rb @@ -1,48 +1,48 @@ require 'puppet/util/adsi' Puppet::Type.type(:group).provide :windows_adsi do desc "Group management for Windows" defaultfor :operatingsystem => :windows confine :operatingsystem => :windows confine :feature => :microsoft_windows has_features :manages_members def group @group ||= Puppet::Util::ADSI::Group.new(@resource[:name]) end def members group.members end def members=(members) group.set_members(members) end def create @group = Puppet::Util::ADSI::Group.create(@resource[:name]) self.members = @resource[:members] end def exists? Puppet::Util::ADSI::Group.exists?(@resource[:name]) end def delete Puppet::Util::ADSI::Group.delete(@resource[:name]) end def gid - nil + Puppet::Util::ADSI.sid_for_account(resource[:name]) end def gid=(value) - warning "No support for managing property gid of group #{@resource[:name]} on Windows" + fail "gid is read-only" end def self.instances Puppet::Util::ADSI::Group.map { |g| new(:ensure => :present, :name => g.name) } end end diff --git a/lib/puppet/provider/user/windows_adsi.rb b/lib/puppet/provider/user/windows_adsi.rb index 9250def59..6b5c5fc4d 100644 --- a/lib/puppet/provider/user/windows_adsi.rb +++ b/lib/puppet/provider/user/windows_adsi.rb @@ -1,71 +1,78 @@ require 'puppet/util/adsi' Puppet::Type.type(:user).provide :windows_adsi do desc "User management for Windows" defaultfor :operatingsystem => :windows confine :operatingsystem => :windows confine :feature => :microsoft_windows has_features :manages_homedir def user @user ||= Puppet::Util::ADSI::User.new(@resource[:name]) end def groups user.groups.join(',') end def groups=(groups) user.set_groups(groups, @resource[:membership] == :minimum) end def create @user = Puppet::Util::ADSI::User.create(@resource[:name]) [:comment, :home, :groups].each do |prop| send("#{prop}=", @resource[prop]) if @resource[prop] end end def exists? Puppet::Util::ADSI::User.exists?(@resource[:name]) end def delete Puppet::Util::ADSI::User.delete(@resource[:name]) end # Only flush if we created or modified a user, not deleted def flush @user.commit if @user end def comment user['Description'] end def comment=(value) user['Description'] = value end def home user['HomeDirectory'] end def home=(value) user['HomeDirectory'] = value end - [:uid, :gid, :shell].each do |prop| - define_method(prop) { nil } + def uid + Puppet::Util::ADSI.sid_for_account(resource[:name]) + end + def uid=(value) + fail "uid is read-only" + end + + [:gid, :shell].each do |prop| + define_method(prop) { nil } define_method("#{prop}=") do |v| - warning "No support for managing property #{prop} of user #{@resource[:name]} on Windows" + fail "No support for managing property #{prop} of user #{@resource[:name]} on Windows" end end def self.instances Puppet::Util::ADSI::User.map { |u| new(:ensure => :present, :name => u.name) } end end diff --git a/lib/puppet/type/group.rb b/lib/puppet/type/group.rb index b534ef275..f3cc97e22 100755 --- a/lib/puppet/type/group.rb +++ b/lib/puppet/type/group.rb @@ -1,145 +1,148 @@ require 'etc' require 'facter' require 'puppet/property/keyvalue' module Puppet newtype(:group) do @doc = "Manage groups. On most platforms this can only create groups. Group membership must be managed on individual users. On some platforms such as OS X, group membership is managed as an attribute of the group, not the user record. Providers must have the feature 'manages_members' to manage the 'members' property of a group record." feature :manages_members, "For directories where membership is an attribute of groups not users." feature :manages_aix_lam, "The provider can manage AIX Loadable Authentication Module (LAM) system." feature :system_groups, "The provider allows you to create system groups with lower GIDs." ensurable do desc "Create or remove the group." newvalue(:present) do provider.create end newvalue(:absent) do provider.delete end end newproperty(:gid) do desc "The group ID. Must be specified numerically. If not specified, a number will be picked, which can result in ID differences across systems and thus is not recommended. The - GID is picked according to local system standards." + GID is picked according to local system standards. + + On Windows, the property will return the group's security + identifier (SID)." def retrieve provider.gid end def sync if self.should == :absent raise Puppet::DevError, "GID cannot be deleted" else provider.gid = self.should end end munge do |gid| case gid when String if gid =~ /^[-0-9]+$/ gid = Integer(gid) else self.fail "Invalid GID #{gid}" end when Symbol unless gid == :absent self.devfail "Invalid GID #{gid}" end end return gid end end newproperty(:members, :array_matching => :all, :required_features => :manages_members) do desc "The members of the group. For directory services where group membership is stored in the group objects, not the users." def change_to_s(currentvalue, newvalue) currentvalue = currentvalue.join(",") if currentvalue != :absent newvalue = newvalue.join(",") super(currentvalue, newvalue) end end newparam(:auth_membership) do desc "whether the provider is authoritative for group membership." defaultto true end newparam(:name) do desc "The group name. While naming limitations vary by system, it is advisable to keep the name to the degenerate limitations, which is a maximum of 8 characters beginning with a letter." isnamevar end newparam(:allowdupe, :boolean => true) do desc "Whether to allow duplicate GIDs. This option does not work on FreeBSD (contract to the `pw` man page)." newvalues(:true, :false) defaultto false end newparam(:ia_load_module, :required_features => :manages_aix_lam) do desc "The name of the I&A module to use to manage this user" defaultto "compat" end newproperty(:attributes, :parent => Puppet::Property::KeyValue, :required_features => :manages_aix_lam) do desc "Specify group AIX attributes in an array of keyvalue pairs" def membership :attribute_membership end def delimiter " " end validate do |value| raise ArgumentError, "Attributes value pairs must be seperated by an =" unless value.include?("=") end end newparam(:attribute_membership) do desc "Whether specified attribute value pairs should be treated as the only attributes of the user or whether they should merely be treated as the minimum list." newvalues(:inclusive, :minimum) defaultto :minimum end newparam(:system, :boolean => true) do desc "Whether the group is a system group with lower GID." newvalues(:true, :false) defaultto false end end end diff --git a/lib/puppet/type/user.rb b/lib/puppet/type/user.rb index c64bf69e8..ea23378ed 100755 --- a/lib/puppet/type/user.rb +++ b/lib/puppet/type/user.rb @@ -1,492 +1,495 @@ require 'etc' require 'facter' require 'puppet/property/list' require 'puppet/property/ordered_list' require 'puppet/property/keyvalue' module Puppet newtype(:user) do @doc = "Manage users. This type is mostly built to manage system users, so it is lacking some features useful for managing normal users. This resource type uses the prescribed native tools for creating groups and generally uses POSIX APIs for retrieving information about them. It does not directly modify `/etc/passwd` or anything. **Autorequires:** If Puppet is managing the user's primary group (as provided in the `gid` attribute), the user resource will autorequire that group. If Puppet is managing any role accounts corresponding to the user's roles, the user resource will autorequire those role accounts." feature :allows_duplicates, "The provider supports duplicate users with the same UID." feature :manages_homedir, "The provider can create and remove home directories." feature :manages_passwords, "The provider can modify user passwords, by accepting a password hash." feature :manages_password_age, "The provider can set age requirements and restrictions for passwords." feature :manages_solaris_rbac, "The provider can manage roles and normal users" feature :manages_expiry, "The provider can manage the expiry date for a user." feature :system_users, "The provider allows you to create system users with lower UIDs." feature :manages_aix_lam, "The provider can manage AIX Loadable Authentication Module (LAM) system." newproperty(:ensure, :parent => Puppet::Property::Ensure) do newvalue(:present, :event => :user_created) do provider.create end newvalue(:absent, :event => :user_removed) do provider.delete end newvalue(:role, :event => :role_created, :required_features => :manages_solaris_rbac) do provider.create_role end desc "The basic state that the object should be in." # If they're talking about the thing at all, they generally want to # say it should exist. defaultto do if @resource.managed? :present else nil end end def retrieve if provider.exists? if provider.respond_to?(:is_role?) and provider.is_role? return :role else return :present end else return :absent end end end newproperty(:home) do desc "The home directory of the user. The directory must be created separately and is not currently checked for existence." end newproperty(:uid) do desc "The user ID. Must be specified numerically. For new users being created, if no user ID is specified then one will be chosen automatically, which will likely result in the same user having different IDs on different systems, which is not recommended. This is especially noteworthy if you use Puppet to manage the same user on both Darwin and other platforms, since Puppet does the ID generation for you on Darwin, but the - tools do so on other platforms." + tools do so on other platforms. + + On Windows, the property will return the user's security + identifier (SID)." munge do |value| case value when String if value =~ /^[-0-9]+$/ value = Integer(value) end end return value end end newproperty(:gid) do desc "The user's primary group. Can be specified numerically or by name." munge do |value| if value.is_a?(String) and value =~ /^[-0-9]+$/ Integer(value) else value end end def insync?(is) # We know the 'is' is a number, so we need to convert the 'should' to a number, # too. @should.each do |value| return true if number = Puppet::Util.gid(value) and is == number end false end def sync found = false @should.each do |value| if number = Puppet::Util.gid(value) provider.gid = number found = true break end end fail "Could not find group(s) #{@should.join(",")}" unless found # Use the default event. end end newproperty(:comment) do desc "A description of the user. Generally is a user's full name." end newproperty(:shell) do desc "The user's login shell. The shell must exist and be executable." end newproperty(:password, :required_features => :manages_passwords) do desc "The user's password, in whatever encrypted format the local machine requires. Be sure to enclose any value that includes a dollar sign ($) in single quotes (\')." validate do |value| raise ArgumentError, "Passwords cannot include ':'" if value.is_a?(String) and value.include?(":") end def change_to_s(currentvalue, newvalue) if currentvalue == :absent return "created password" else return "changed password" end end def is_to_s( currentvalue ) return '[old password hash redacted]' end def should_to_s( newvalue ) return '[new password hash redacted]' end end newproperty(:password_min_age, :required_features => :manages_password_age) do desc "The minimum amount of time in days a password must be used before it may be changed" munge do |value| case value when String Integer(value) else value end end validate do |value| if value.to_s !~ /^-?\d+$/ raise ArgumentError, "Password minimum age must be provided as a number" end end end newproperty(:password_max_age, :required_features => :manages_password_age) do desc "The maximum amount of time in days a password may be used before it must be changed" munge do |value| case value when String Integer(value) else value end end validate do |value| if value.to_s !~ /^-?\d+$/ raise ArgumentError, "Password maximum age must be provided as a number" end end end newproperty(:groups, :parent => Puppet::Property::List) do desc "The groups of which the user is a member. The primary group should not be listed. Multiple groups should be specified as an array." validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Group names must be provided, not numbers" end raise ArgumentError, "Group names must be provided as an array, not a comma-separated list" if value.include?(",") end end newparam(:name) do desc "User name. While limitations are determined for each operating system, it is generally a good idea to keep to the degenerate 8 characters, beginning with a letter." isnamevar end newparam(:membership) do desc "Whether specified groups should be treated as the only groups of which the user is a member or whether they should merely be treated as the minimum membership list." newvalues(:inclusive, :minimum) defaultto :minimum end newparam(:system, :boolean => true) do desc "Whether the user is a system user with lower UID." newvalues(:true, :false) defaultto false end newparam(:allowdupe, :boolean => true) do desc "Whether to allow duplicate UIDs." newvalues(:true, :false) defaultto false end newparam(:managehome, :boolean => true) do desc "Whether to manage the home directory when managing the user." newvalues(:true, :false) defaultto false validate do |val| if val.to_s == "true" raise ArgumentError, "User provider #{provider.class.name} can not manage home directories" unless provider.class.manages_homedir? end end end newproperty(:expiry, :required_features => :manages_expiry) do desc "The expiry date for this user. Must be provided in a zero padded YYYY-MM-DD format - e.g 2010-02-19." validate do |value| if value !~ /^\d{4}-\d{2}-\d{2}$/ raise ArgumentError, "Expiry dates must be YYYY-MM-DD" end end end # Autorequire the group, if it's around autorequire(:group) do autos = [] if obj = @parameters[:gid] and groups = obj.shouldorig groups = groups.collect { |group| if group =~ /^\d+$/ Integer(group) else group end } groups.each { |group| case group when Integer if resource = catalog.resources.find { |r| r.is_a?(Puppet::Type.type(:group)) and r.should(:gid) == group } autos << resource end else autos << group end } end if obj = @parameters[:groups] and groups = obj.should autos += groups.split(",") end autos end # Provide an external hook. Yay breaking out of APIs. def exists? provider.exists? end def retrieve absent = false properties.inject({}) { |prophash, property| current_value = :absent if absent prophash[property] = :absent else current_value = property.retrieve prophash[property] = current_value end if property.name == :ensure and current_value == :absent absent = true end prophash } end newproperty(:roles, :parent => Puppet::Property::List, :required_features => :manages_solaris_rbac) do desc "The roles the user has. Multiple roles should be specified as an array." def membership :role_membership end validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Role names must be provided, not numbers" end raise ArgumentError, "Role names must be provided as an array, not a comma-separated list" if value.include?(",") end end #autorequire the roles that the user has autorequire(:user) do reqs = [] if roles_property = @parameters[:roles] and roles = roles_property.should reqs += roles.split(',') end reqs end newparam(:role_membership) do desc "Whether specified roles should be treated as the only roles of which the user is a member or whether they should merely be treated as the minimum membership list." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:auths, :parent => Puppet::Property::List, :required_features => :manages_solaris_rbac) do desc "The auths the user has. Multiple auths should be specified as an array." def membership :auth_membership end validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Auth names must be provided, not numbers" end raise ArgumentError, "Auth names must be provided as an array, not a comma-separated list" if value.include?(",") end end newparam(:auth_membership) do desc "Whether specified auths should be treated as the only auths of which the user is a member or whether they should merely be treated as the minimum membership list." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:profiles, :parent => Puppet::Property::OrderedList, :required_features => :manages_solaris_rbac) do desc "The profiles the user has. Multiple profiles should be specified as an array." def membership :profile_membership end validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Profile names must be provided, not numbers" end raise ArgumentError, "Profile names must be provided as an array, not a comma-separated list" if value.include?(",") end end newparam(:profile_membership) do desc "Whether specified roles should be treated as the only roles of which the user is a member or whether they should merely be treated as the minimum membership list." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:keys, :parent => Puppet::Property::KeyValue, :required_features => :manages_solaris_rbac) do desc "Specify user attributes in an array of keyvalue pairs" def membership :key_membership end validate do |value| raise ArgumentError, "key value pairs must be seperated by an =" unless value.include?("=") end end newparam(:key_membership) do desc "Whether specified key value pairs should be treated as the only attributes of the user or whether they should merely be treated as the minimum list." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:project, :required_features => :manages_solaris_rbac) do desc "The name of the project associated with a user" end newparam(:ia_load_module, :required_features => :manages_aix_lam) do desc "The name of the I&A module to use to manage this user" end newproperty(:attributes, :parent => Puppet::Property::KeyValue, :required_features => :manages_aix_lam) do desc "Specify user AIX attributes in an array of keyvalue pairs" def membership :attribute_membership end def delimiter " " end validate do |value| raise ArgumentError, "Attributes value pairs must be seperated by an =" unless value.include?("=") end end newparam(:attribute_membership) do desc "Whether specified attribute value pairs should be treated as the only attributes of the user or whether they should merely be treated as the minimum list." newvalues(:inclusive, :minimum) defaultto :minimum end end end diff --git a/lib/puppet/util/adsi.rb b/lib/puppet/util/adsi.rb index f865743e2..504057903 100644 --- a/lib/puppet/util/adsi.rb +++ b/lib/puppet/util/adsi.rb @@ -1,278 +1,288 @@ module Puppet::Util::ADSI class << self def connectable?(uri) begin !! connect(uri) rescue false end end def connect(uri) begin WIN32OLE.connect(uri) rescue Exception => e raise Puppet::Error.new( "ADSI connection error: #{e}" ) end end def create(name, resource_type) Puppet::Util::ADSI.connect(computer_uri).Create(resource_type, name) end def delete(name, resource_type) Puppet::Util::ADSI.connect(computer_uri).Delete(resource_type, name) end def computer_name unless @computer_name buf = " " * 128 Win32API.new('kernel32', 'GetComputerName', ['P','P'], 'I').call(buf, buf.length.to_s) @computer_name = buf.unpack("A*") end @computer_name end def computer_uri "WinNT://#{computer_name}" end def wmi_resource_uri( host = '.' ) "winmgmts:{impersonationLevel=impersonate}!//#{host}/root/cimv2" end def uri(resource_name, resource_type) "#{computer_uri}/#{resource_name},#{resource_type}" end def execquery(query) connect(wmi_resource_uri).execquery(query) end + + def sid_for_account(name) + sid = nil + execquery( "SELECT Sid from Win32_Account WHERE Name = '#{name}' + AND LocalAccount = true").each { |u| + sid ||= u.Sid + } + sid + end + end class User extend Enumerable attr_accessor :native_user attr_reader :name def initialize(name, native_user = nil) @name = name @native_user = native_user end def native_user @native_user ||= Puppet::Util::ADSI.connect(uri) end def self.uri(name) Puppet::Util::ADSI.uri(name, 'user') end def uri self.class.uri(name) end def self.logon(name, password) fLOGON32_LOGON_NETWORK = 3 fLOGON32_PROVIDER_DEFAULT = 0 logon_user = Win32API.new("advapi32", "LogonUser", ['P', 'P', 'P', 'L', 'L', 'P'], 'L') close_handle = Win32API.new("kernel32", "CloseHandle", ['P'], 'V') token = ' ' * 4 if logon_user.call(name, "", password, fLOGON32_LOGON_NETWORK, fLOGON32_PROVIDER_DEFAULT, token) != 0 close_handle.call(token.unpack('L')[0]) true else false end end def [](attribute) native_user.Get(attribute) end def []=(attribute, value) native_user.Put(attribute, value) end def commit begin native_user.SetInfo unless native_user.nil? rescue Exception => e raise Puppet::Error.new( "User update failed: #{e}" ) end self end def password_is?(password) self.class.logon(name, password) end def add_flag(flag_name, value) flag = native_user.Get(flag_name) rescue 0 native_user.Put(flag_name, flag | value) commit end def password=(password) native_user.SetPassword(password) commit fADS_UF_DONT_EXPIRE_PASSWD = 0x10000 add_flag("UserFlags", fADS_UF_DONT_EXPIRE_PASSWD) end def groups # WIN32OLE objects aren't enumerable, so no map groups = [] native_user.Groups.each {|g| groups << g.Name} groups end def add_to_groups(*group_names) group_names.each do |group_name| Puppet::Util::ADSI::Group.new(group_name).add_member(@name) end end alias add_to_group add_to_groups def remove_from_groups(*group_names) group_names.each do |group_name| Puppet::Util::ADSI::Group.new(group_name).remove_member(@name) end end alias remove_from_group remove_from_groups def set_groups(desired_groups, minimum = true) return if desired_groups.nil? or desired_groups.empty? desired_groups = desired_groups.split(',').map(&:strip) current_groups = self.groups # First we add the user to all the groups it should be in but isn't groups_to_add = desired_groups - current_groups add_to_groups(*groups_to_add) # Then we remove the user from all groups it is in but shouldn't be, if # that's been requested groups_to_remove = current_groups - desired_groups remove_from_groups(*groups_to_remove) unless minimum end def self.create(name) new(name, Puppet::Util::ADSI.create(name, 'user')) end def self.exists?(name) Puppet::Util::ADSI::connectable?(User.uri(name)) end def self.delete(name) Puppet::Util::ADSI.delete(name, 'user') end def self.each(&block) wql = Puppet::Util::ADSI.execquery("select * from win32_useraccount") users = [] wql.each do |u| users << new(u.name, u) end users.each(&block) end end class Group extend Enumerable attr_accessor :native_group attr_reader :name def initialize(name, native_group = nil) @name = name @native_group = native_group end def uri self.class.uri(name) end def self.uri(name) Puppet::Util::ADSI.uri(name, 'group') end def native_group @native_group ||= Puppet::Util::ADSI.connect(uri) end def commit begin native_group.SetInfo unless native_group.nil? rescue Exception => e raise Puppet::Error.new( "Group update failed: #{e}" ) end self end def add_members(*names) names.each do |name| native_group.Add(Puppet::Util::ADSI::User.uri(name)) end end alias add_member add_members def remove_members(*names) names.each do |name| native_group.Remove(Puppet::Util::ADSI::User.uri(name)) end end alias remove_member remove_members def members # WIN32OLE objects aren't enumerable, so no map members = [] native_group.Members.each {|m| members << m.Name} members end def set_members(desired_members) return if desired_members.nil? or desired_members.empty? current_members = self.members # First we add all missing members members_to_add = desired_members - current_members add_members(*members_to_add) # Then we remove all extra members members_to_remove = current_members - desired_members remove_members(*members_to_remove) end def self.create(name) new(name, Puppet::Util::ADSI.create(name, 'group')) end def self.exists?(name) Puppet::Util::ADSI.connectable?(Group.uri(name)) end def self.delete(name) Puppet::Util::ADSI.delete(name, 'group') end def self.each(&block) wql = Puppet::Util::ADSI.execquery( "select * from win32_group" ) groups = [] wql.each do |g| groups << new(g.name, g) end groups.each(&block) end end end diff --git a/spec/unit/provider/group/windows_adsi_spec.rb b/spec/unit/provider/group/windows_adsi_spec.rb index 7faaa1a8c..6811c8078 100644 --- a/spec/unit/provider/group/windows_adsi_spec.rb +++ b/spec/unit/provider/group/windows_adsi_spec.rb @@ -1,79 +1,84 @@ #!/usr/bin/env ruby require 'spec_helper' describe Puppet::Type.type(:group).provider(:windows_adsi) do let(:resource) do Puppet::Type.type(:group).new( :title => 'testers', :provider => :windows_adsi ) end let(:provider) { resource.provider } let(:connection) { stub 'connection' } before :each do Puppet::Util::ADSI.stubs(:computer_name).returns('testcomputername') Puppet::Util::ADSI.stubs(:connect).returns connection end describe ".instances" do it "should enumerate all groups" do names = ['group1', 'group2', 'group3'] stub_groups = names.map{|n| stub(:name => n)} connection.stubs(:execquery).with("select * from win32_group").returns stub_groups described_class.instances.map(&:name).should =~ names end end describe "when managing members" do it "should be able to provide a list of members" do provider.group.stubs(:members).returns ['user1', 'user2', 'user3'] provider.members.should =~ ['user1', 'user2', 'user3'] end it "should be able to set group members" do provider.group.stubs(:members).returns ['user1', 'user2'] provider.group.expects(:remove_members).with('user1') provider.group.expects(:add_members).with('user3') provider.members = ['user2', 'user3'] end end it "should be able to create a group" do resource[:members] = ['user1', 'user2'] group = stub 'group' Puppet::Util::ADSI::Group.expects(:create).with('testers').returns group group.expects(:set_members).with(['user1', 'user2']) provider.create end it "should be able to test whether a group exists" do Puppet::Util::ADSI.stubs(:connect).returns stub('connection') provider.should be_exists Puppet::Util::ADSI.stubs(:connect).returns nil provider.should_not be_exists end it "should be able to delete a group" do connection.expects(:Delete).with('group', 'testers') provider.delete end - it "should warn when trying to manage the gid property" do - provider.expects(:warning).with { |msg| msg =~ /No support for managing property gid/ } + it "should report the group's SID as gid" do + Puppet::Util::ADSI.expects(:sid_for_account).with('testers').returns('S-1-5-32-547') + provider.gid.should == 'S-1-5-32-547' + end + + it "should fail when trying to manage the gid property" do + provider.expects(:fail).with { |msg| msg =~ /gid is read-only/ } provider.send(:gid=, 500) end end diff --git a/spec/unit/provider/user/windows_adsi_spec.rb b/spec/unit/provider/user/windows_adsi_spec.rb index 073a3d328..ad24571c5 100644 --- a/spec/unit/provider/user/windows_adsi_spec.rb +++ b/spec/unit/provider/user/windows_adsi_spec.rb @@ -1,110 +1,121 @@ #!/usr/bin/env ruby require 'spec_helper' describe Puppet::Type.type(:user).provider(:windows_adsi) do let(:resource) do Puppet::Type.type(:user).new( :title => 'testuser', :comment => 'Test J. User', :provider => :windows_adsi ) end let(:provider) { resource.provider } let(:connection) { stub 'connection' } before :each do Puppet::Util::ADSI.stubs(:computer_name).returns('testcomputername') Puppet::Util::ADSI.stubs(:connect).returns connection end describe ".instances" do it "should enumerate all users" do names = ['user1', 'user2', 'user3'] stub_users = names.map{|n| stub(:name => n)} connection.stubs(:execquery).with("select * from win32_useraccount").returns(stub_users) described_class.instances.map(&:name).should =~ names end end it "should provide access to a Puppet::Util::ADSI::User object" do provider.user.should be_a(Puppet::Util::ADSI::User) end describe "when managing groups" do it 'should return the list of groups as a comma-separated list' do provider.user.stubs(:groups).returns ['group1', 'group2', 'group3'] provider.groups.should == 'group1,group2,group3' end it "should return absent if there are no groups" do provider.user.stubs(:groups).returns [] provider.groups.should == '' end it 'should be able to add a user to a set of groups' do resource[:membership] = :minimum provider.user.expects(:set_groups).with('group1,group2', true) provider.groups = 'group1,group2' resource[:membership] = :inclusive provider.user.expects(:set_groups).with('group1,group2', false) provider.groups = 'group1,group2' end end describe "when creating a user" do it "should create the user on the system and set its other properties" do resource[:groups] = ['group1', 'group2'] resource[:membership] = :inclusive resource[:comment] = 'a test user' resource[:home] = 'C:\Users\testuser' user = stub 'user' Puppet::Util::ADSI::User.expects(:create).with('testuser').returns user user.stubs(:groups).returns(['group2', 'group3']) user.expects(:set_groups).with('group1,group2', false) user.expects(:[]=).with('Description', 'a test user') user.expects(:[]=).with('HomeDirectory', 'C:\Users\testuser') provider.create end end it 'should be able to test whether a user exists' do Puppet::Util::ADSI.stubs(:connect).returns stub('connection') provider.should be_exists Puppet::Util::ADSI.stubs(:connect).returns nil provider.should_not be_exists end it 'should be able to delete a user' do connection.expects(:Delete).with('user', 'testuser') provider.delete end it "should commit the user when flushed" do provider.user.expects(:commit) provider.flush end - [:uid, :gid, :shell].each do |prop| - it "should warn when trying to manage the #{prop} property" do - provider.expects(:warning).with { |msg| msg =~ /No support for managing property #{prop}/ } + it "should return the user's SID as uid" do + Puppet::Util::ADSI.expects(:sid_for_account).with('testuser').returns('S-1-5-21-1362942247-2130103807-3279964888-1111') + + provider.uid.should == 'S-1-5-21-1362942247-2130103807-3279964888-1111' + end + + it "should fail when trying to manage the uid property" do + provider.expects(:fail).with { |msg| msg =~ /uid is read-only/ } + provider.send(:uid=, 500) + end + + [:gid, :shell].each do |prop| + it "should fail when trying to manage the #{prop} property" do + provider.expects(:fail).with { |msg| msg =~ /No support for managing property #{prop}/ } provider.send("#{prop}=", 'foo') end end end diff --git a/spec/unit/util/adsi_spec.rb b/spec/unit/util/adsi_spec.rb index b61724405..0624c9b4f 100644 --- a/spec/unit/util/adsi_spec.rb +++ b/spec/unit/util/adsi_spec.rb @@ -1,202 +1,207 @@ #!/usr/bin/env ruby require 'spec_helper' require 'puppet/util/adsi' describe Puppet::Util::ADSI do let(:connection) { stub 'connection' } before(:each) do Puppet::Util::ADSI.instance_variable_set(:@computer_name, 'testcomputername') Puppet::Util::ADSI.stubs(:connect).returns connection end it "should generate the correct URI for a resource" do Puppet::Util::ADSI.uri('test', 'user').should == "WinNT://testcomputername/test,user" end it "should be able to get the name of the computer" do Puppet::Util::ADSI.computer_name.should == 'testcomputername' end it "should be able to provide the correct WinNT base URI for the computer" do Puppet::Util::ADSI.computer_uri.should == "WinNT://testcomputername" end + it "should return a SID for a passed user or group name" do + Puppet::Util::ADSI.expects(:execquery).returns([stub('acct_id', :Sid => 'S-1-5-32-547')]) + Puppet::Util::ADSI.sid_for_account('testers').should == 'S-1-5-32-547' + end + describe Puppet::Util::ADSI::User do let(:username) { 'testuser' } it "should generate the correct URI" do Puppet::Util::ADSI::User.uri(username).should == "WinNT://testcomputername/#{username},user" end it "should be able to create a user" do adsi_user = stub('adsi') connection.expects(:Create).with('user', username).returns(adsi_user) user = Puppet::Util::ADSI::User.create(username) user.should be_a(Puppet::Util::ADSI::User) user.native_user.should == adsi_user end it "should be able to check the existence of a user" do Puppet::Util::ADSI.expects(:connect).with("WinNT://testcomputername/#{username},user").returns connection Puppet::Util::ADSI::User.exists?(username).should be_true end it "should be able to delete a user" do connection.expects(:Delete).with('user', username) Puppet::Util::ADSI::User.delete(username) end describe "an instance" do let(:adsi_user) { stub 'user' } let(:user) { Puppet::Util::ADSI::User.new(username, adsi_user) } it "should provide its groups as a list of names" do names = ["group1", "group2"] groups = names.map { |name| mock('group', :Name => name) } adsi_user.expects(:Groups).returns(groups) user.groups.should =~ names end it "should be able to test whether a given password is correct" do Puppet::Util::ADSI::User.expects(:logon).with(username, 'pwdwrong').returns(false) Puppet::Util::ADSI::User.expects(:logon).with(username, 'pwdright').returns(true) user.password_is?('pwdwrong').should be_false user.password_is?('pwdright').should be_true end it "should be able to set a password" do adsi_user.expects(:SetPassword).with('pwd') adsi_user.expects(:SetInfo).at_least_once flagname = "UserFlags" fADS_UF_DONT_EXPIRE_PASSWD = 0x10000 adsi_user.expects(:Get).with(flagname).returns(0) adsi_user.expects(:Put).with(flagname, fADS_UF_DONT_EXPIRE_PASSWD) user.password = 'pwd' end it "should generate the correct URI" do user.uri.should == "WinNT://testcomputername/#{username},user" end describe "when given a set of groups to which to add the user" do let(:groups_to_set) { 'group1,group2' } before(:each) do user.expects(:groups).returns ['group2', 'group3'] end describe "if membership is specified as inclusive" do it "should add the user to those groups, and remove it from groups not in the list" do group1 = stub 'group1' group1.expects(:Add).with("WinNT://testcomputername/#{username},user") group3 = stub 'group1' group3.expects(:Remove).with("WinNT://testcomputername/#{username},user") Puppet::Util::ADSI.expects(:connect).with('WinNT://testcomputername/group1,group').returns group1 Puppet::Util::ADSI.expects(:connect).with('WinNT://testcomputername/group3,group').returns group3 user.set_groups(groups_to_set, false) end end describe "if membership is specified as minimum" do it "should add the user to the specified groups without affecting its other memberships" do group1 = stub 'group1' group1.expects(:Add).with("WinNT://testcomputername/#{username},user") Puppet::Util::ADSI.expects(:connect).with('WinNT://testcomputername/group1,group').returns group1 user.set_groups(groups_to_set, true) end end end end end describe Puppet::Util::ADSI::Group do let(:groupname) { 'testgroup' } describe "an instance" do let(:adsi_group) { stub 'group' } let(:group) { Puppet::Util::ADSI::Group.new(groupname, adsi_group) } it "should be able to add a member" do adsi_group.expects(:Add).with("WinNT://testcomputername/someone,user") group.add_member('someone') end it "should be able to remove a member" do adsi_group.expects(:Remove).with("WinNT://testcomputername/someone,user") group.remove_member('someone') end it "should provide its groups as a list of names" do names = ['user1', 'user2'] users = names.map { |name| mock('user', :Name => name) } adsi_group.expects(:Members).returns(users) group.members.should =~ names end it "should be able to add a list of users to a group" do names = ['user1', 'user2'] adsi_group.expects(:Members).returns names.map{|n| stub(:Name => n)} adsi_group.expects(:Remove).with('WinNT://testcomputername/user1,user') adsi_group.expects(:Add).with('WinNT://testcomputername/user3,user') group.set_members(['user2', 'user3']) end it "should generate the correct URI" do group.uri.should == "WinNT://testcomputername/#{groupname},group" end end it "should generate the correct URI" do Puppet::Util::ADSI::Group.uri("people").should == "WinNT://testcomputername/people,group" end it "should be able to create a group" do adsi_group = stub("adsi") connection.expects(:Create).with('group', groupname).returns(adsi_group) group = Puppet::Util::ADSI::Group.create(groupname) group.should be_a(Puppet::Util::ADSI::Group) group.native_group.should == adsi_group end it "should be able to confirm the existence of a group" do Puppet::Util::ADSI.expects(:connect).with("WinNT://testcomputername/#{groupname},group").returns connection Puppet::Util::ADSI::Group.exists?(groupname).should be_true end it "should be able to delete a group" do connection.expects(:Delete).with('group', groupname) Puppet::Util::ADSI::Group.delete(groupname) end end end