File: sid.rb

package info (click to toggle)
puppet-agent 7.23.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 19,092 kB
  • sloc: ruby: 245,074; sh: 456; makefile: 38; xml: 33
file content (287 lines) | stat: -rw-r--r-- 11,150 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
require_relative '../../../puppet/util/windows'

module Puppet::Util::Windows
  module SID
    require 'ffi'
    extend FFI::Library

    # missing from Windows::Error
    ERROR_NONE_MAPPED           = 1332
    ERROR_INVALID_SID_STRUCTURE = 1337

    # Well Known SIDs
    Null                        = 'S-1-0'
    Nobody                      = 'S-1-0-0'
    World                       = 'S-1-1'
    Everyone                    = 'S-1-1-0'
    Local                       = 'S-1-2'
    Creator                     = 'S-1-3'
    CreatorOwner                = 'S-1-3-0'
    CreatorGroup                = 'S-1-3-1'
    CreatorOwnerServer          = 'S-1-3-2'
    CreatorGroupServer          = 'S-1-3-3'
    NonUnique                   = 'S-1-4'
    Nt                          = 'S-1-5'
    Dialup                      = 'S-1-5-1'
    Network                     = 'S-1-5-2'
    Batch                       = 'S-1-5-3'
    Interactive                 = 'S-1-5-4'
    Service                     = 'S-1-5-6'
    Anonymous                   = 'S-1-5-7'
    Proxy                       = 'S-1-5-8'
    EnterpriseDomainControllers = 'S-1-5-9'
    PrincipalSelf               = 'S-1-5-10'
    AuthenticatedUsers          = 'S-1-5-11'
    RestrictedCode              = 'S-1-5-12'
    TerminalServerUsers         = 'S-1-5-13'
    LocalSystem                 = 'S-1-5-18'
    NtLocal                     = 'S-1-5-19'
    NtNetwork                   = 'S-1-5-20'
    BuiltinAdministrators       = 'S-1-5-32-544'
    BuiltinUsers                = 'S-1-5-32-545'
    Guests                      = 'S-1-5-32-546'
    PowerUsers                  = 'S-1-5-32-547'
    AccountOperators            = 'S-1-5-32-548'
    ServerOperators             = 'S-1-5-32-549'
    PrintOperators              = 'S-1-5-32-550'
    BackupOperators             = 'S-1-5-32-551'
    Replicators                 = 'S-1-5-32-552'
    AllAppPackages              = 'S-1-15-2-1'

    # Convert an account name, e.g. 'Administrators' into a SID string,
    # e.g. 'S-1-5-32-544'. The name can be specified as 'Administrators',
    # 'BUILTIN\Administrators', or 'S-1-5-32-544', and will return the
    # SID. Returns nil if the account doesn't exist.
    def name_to_sid(name)
      sid = name_to_principal(name)

      sid ? sid.sid : nil
    end
    module_function :name_to_sid

    # Convert an account name, e.g. 'Administrators' into a Principal::SID object,
    # e.g. 'S-1-5-32-544'. The name can be specified as 'Administrators',
    # 'BUILTIN\Administrators', or 'S-1-5-32-544', and will return the
    # SID object. Returns nil if the account doesn't exist.
    # This method returns a SID::Principal with the account, domain, SID, etc
    def name_to_principal(name, allow_unresolved = false)
      # Apparently, we accept a symbol..
      name = name.to_s.strip if name

      # if name is a SID string, convert it to raw bytes for use with lookup_account_sid
      raw_sid_bytes = nil
      begin
        string_to_sid_ptr(name) do |sid_ptr|
          raw_sid_bytes = sid_ptr.read_array_of_uchar(get_length_sid(sid_ptr))
        end
      rescue => e
        # Avoid debug logs pollution with valid account names
        # https://docs.microsoft.com/en-us/windows/win32/api/sddl/nf-sddl-convertstringsidtosidw#return-value
        Puppet.debug("Could not retrieve raw SID bytes from '#{name}': #{e.message}") unless e.code == ERROR_INVALID_SID_STRUCTURE
      end

      raw_sid_bytes ? Principal.lookup_account_sid(raw_sid_bytes) : Principal.lookup_account_name(name)
    rescue => e
      Puppet.debug("#{e.message}")
      (allow_unresolved && raw_sid_bytes) ? unresolved_principal(name, raw_sid_bytes) : nil
    end
    module_function :name_to_principal
    class << self; alias name_to_sid_object name_to_principal; end

    # Converts an octet string array of bytes to a SID::Principal object,
    # e.g. [1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0] is the representation for
    # S-1-5-18, the local 'SYSTEM' account.
    # Raises an Error for nil or non-array input.
    # This method returns a SID::Principal with the account, domain, SID, etc
    def octet_string_to_principal(bytes)
      if !bytes || !bytes.respond_to?('pack') || bytes.empty?
        raise Puppet::Util::Windows::Error.new(_("Octet string must be an array of bytes"))
      end

      Principal.lookup_account_sid(bytes)
    end
    module_function :octet_string_to_principal
    class << self; alias octet_string_to_sid_object octet_string_to_principal; end

    # Converts a COM instance of IAdsUser or IAdsGroup to a SID::Principal object,
    # Raises an Error for nil or an object without an objectSID / Name property.
    # This method returns a SID::Principal with the account, domain, SID, etc
    # This method will return instances even when the SID is unresolvable, as
    # may be the case when domain users have been added to local groups, but
    # removed from the domain
    def ads_to_principal(ads_object)
      if !ads_object || !ads_object.respond_to?(:ole_respond_to?) ||
        !ads_object.ole_respond_to?(:objectSID) || !ads_object.ole_respond_to?(:Name)
        raise Puppet::Error.new("ads_object must be an IAdsUser or IAdsGroup instance")
      end
      octet_string_to_principal(ads_object.objectSID)
    rescue Puppet::Util::Windows::Error => e
      # if the error is not a lookup / mapping problem, immediately re-raise
      raise if e.code != ERROR_NONE_MAPPED

      # if the Name property isn't formatted like a SID, OR
      if !valid_sid?(ads_object.Name) ||
        # if the objectSID doesn't match the Name property, also raise
        ((converted = octet_string_to_sid_string(ads_object.objectSID)) != ads_object.Name)
        raise Puppet::Error.new("ads_object Name: #{ads_object.Name} invalid or does not match objectSID: #{ads_object.objectSID} (#{converted})", e)
      end

      unresolved_principal(ads_object.Name, ads_object.objectSID)
    end
    module_function :ads_to_principal

    # Convert a SID string, e.g. "S-1-5-32-544" to a name,
    # e.g. 'BUILTIN\Administrators'. Returns nil if an account
    # for that SID does not exist.
    def sid_to_name(value)

      sid_bytes = []
      begin
        string_to_sid_ptr(value) do |ptr|
          sid_bytes = ptr.read_array_of_uchar(get_length_sid(ptr))
        end
      rescue Puppet::Util::Windows::Error => e
        raise if e.code != ERROR_INVALID_SID_STRUCTURE
      end

      Principal.lookup_account_sid(sid_bytes).domain_account
    rescue
      nil
    end
    module_function :sid_to_name

    # https://stackoverflow.com/a/1792930 - 68 bytes, 184 characters in a string
    MAXIMUM_SID_STRING_LENGTH = 184

    # Convert a SID pointer to a SID string, e.g. "S-1-5-32-544".
    def sid_ptr_to_string(psid)
      if ! psid.kind_of?(FFI::Pointer) || IsValidSid(psid) == FFI::WIN32_FALSE
        raise Puppet::Util::Windows::Error.new(_("Invalid SID"))
      end

      sid_string = nil
      FFI::MemoryPointer.new(:pointer, 1) do |buffer_ptr|
        if ConvertSidToStringSidW(psid, buffer_ptr) == FFI::WIN32_FALSE
          raise Puppet::Util::Windows::Error.new(_("Failed to convert binary SID"))
        end

        buffer_ptr.read_win32_local_pointer do |wide_string_ptr|
          if wide_string_ptr.null?
            raise Puppet::Error.new(_("ConvertSidToStringSidW failed to allocate buffer for sid"))
          end

          sid_string = wide_string_ptr.read_arbitrary_wide_string_up_to(MAXIMUM_SID_STRING_LENGTH)
        end
      end

      sid_string
    end
    module_function :sid_ptr_to_string

    # Convert a SID string, e.g. "S-1-5-32-544" to a pointer (containing the
    # address of the binary SID structure). The returned value can be used in
    # Win32 APIs that expect a PSID, e.g. IsValidSid. The account for this
    # SID may or may not exist.
    def string_to_sid_ptr(string_sid, &block)
      FFI::MemoryPointer.from_string_to_wide_string(string_sid) do |lpcwstr|
        FFI::MemoryPointer.new(:pointer, 1) do |sid_ptr_ptr|

          if ConvertStringSidToSidW(lpcwstr, sid_ptr_ptr) == FFI::WIN32_FALSE
            raise Puppet::Util::Windows::Error.new(_("Failed to convert string SID: %{string_sid}") % { string_sid: string_sid })
          end

          sid_ptr_ptr.read_win32_local_pointer do |sid_ptr|
            yield sid_ptr
          end
        end
      end

      # yielded sid_ptr has already had LocalFree called, nothing to return
      nil
    end
    module_function :string_to_sid_ptr

    # Return true if the string is a valid SID, e.g. "S-1-5-32-544", false otherwise.
    def valid_sid?(string_sid)
      valid = false

      begin
        string_to_sid_ptr(string_sid) { |ptr| valid = ! ptr.nil? && ! ptr.null? }
      rescue Puppet::Util::Windows::Error => e
        raise if e.code != ERROR_INVALID_SID_STRUCTURE
      end

      valid
    end
    module_function :valid_sid?

    def get_length_sid(sid_ptr)
      # MSDN states IsValidSid should be called on pointer first
      if ! sid_ptr.kind_of?(FFI::Pointer) || IsValidSid(sid_ptr) == FFI::WIN32_FALSE
        raise Puppet::Util::Windows::Error.new(_("Invalid SID"))
      end

      GetLengthSid(sid_ptr)
    end
    module_function :get_length_sid

    def octet_string_to_sid_string(sid_bytes)
      sid_string = nil

      FFI::MemoryPointer.new(:byte, sid_bytes.length) do |sid_ptr|
        sid_ptr.write_array_of_uchar(sid_bytes)
        sid_string = Puppet::Util::Windows::SID.sid_ptr_to_string(sid_ptr)
      end

      sid_string
    end
    module_function :octet_string_to_sid_string

    # @api private
    def self.unresolved_principal(name, sid_bytes)
      Principal.new(
        name, # account
        sid_bytes, # sid_bytes
        name, # sid string
        nil, #domain
        # https://msdn.microsoft.com/en-us/library/cc245534.aspx?f=255&MSPPError=-2147217396
        # Indicates that the type of object could not be determined. For example, no object with that SID exists.
        :SidTypeUnknown)
    end

    ffi_convention :stdcall

    # https://msdn.microsoft.com/en-us/library/windows/desktop/aa379151(v=vs.85).aspx
    # BOOL WINAPI IsValidSid(
    #   _In_  PSID pSid
    # );
    ffi_lib :advapi32
    attach_function_private :IsValidSid,
      [:pointer], :win32_bool

    # https://msdn.microsoft.com/en-us/library/windows/desktop/aa376399(v=vs.85).aspx
    # BOOL ConvertSidToStringSid(
    #   _In_   PSID Sid,
    #   _Out_  LPTSTR *StringSid
    # );
    ffi_lib :advapi32
    attach_function_private :ConvertSidToStringSidW,
      [:pointer, :pointer], :win32_bool

    # https://msdn.microsoft.com/en-us/library/windows/desktop/aa376402(v=vs.85).aspx
    # BOOL WINAPI ConvertStringSidToSid(
    #   _In_   LPCTSTR StringSid,
    #   _Out_  PSID *Sid
    # );
    ffi_lib :advapi32
    attach_function_private :ConvertStringSidToSidW,
      [:lpcwstr, :pointer], :win32_bool

    # https://msdn.microsoft.com/en-us/library/windows/desktop/aa446642(v=vs.85).aspx
    # DWORD WINAPI GetLengthSid(
    #   _In_ PSID pSid
    # );
    ffi_lib :advapi32
    attach_function_private :GetLengthSid, [:pointer], :dword
  end
end