diff --git a/spec/std/system/group_spec.cr b/spec/std/system/group_spec.cr index 5c55611e4d28..ba511d03a05c 100644 --- a/spec/std/system/group_spec.cr +++ b/spec/std/system/group_spec.cr @@ -1,10 +1,14 @@ -{% skip_file if flag?(:win32) %} - require "spec" require "system/group" -GROUP_NAME = {{ `id -gn`.stringify.chomp }} -GROUP_ID = {{ `id -g`.stringify.chomp }} +{% if flag?(:win32) %} + GROUP_NAME = "BUILTIN\\Administrators" + GROUP_ID = "S-1-5-32-544" +{% else %} + GROUP_NAME = {{ `id -gn`.stringify.chomp }} + GROUP_ID = {{ `id -g`.stringify.chomp }} +{% end %} + INVALID_GROUP_NAME = "this_group_does_not_exist" INVALID_GROUP_ID = {% if flag?(:android) %}"8888"{% else %}"1234567"{% end %} diff --git a/src/crystal/system/group.cr b/src/crystal/system/group.cr index 8a542e2cc63c..6cb93739a900 100644 --- a/src/crystal/system/group.cr +++ b/src/crystal/system/group.cr @@ -12,6 +12,8 @@ end require "./wasi/group" {% elsif flag?(:unix) %} require "./unix/group" +{% elsif flag?(:win32) %} + require "./win32/group" {% else %} {% raise "No Crystal::System::Group implementation available" %} {% end %} diff --git a/src/crystal/system/win32/group.cr b/src/crystal/system/win32/group.cr new file mode 100644 index 000000000000..3b40774ac2d8 --- /dev/null +++ b/src/crystal/system/win32/group.cr @@ -0,0 +1,82 @@ +require "crystal/system/windows" + +# This file contains source code derived from the following: +# +# * https://cs.opensource.google/go/go/+/refs/tags/go1.23.0:src/os/user/lookup_windows.go +# * https://cs.opensource.google/go/go/+/refs/tags/go1.23.0:src/syscall/security_windows.go +# +# The following is their license: +# +# Copyright 2009 The Go Authors. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google LLC nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +module Crystal::System::Group + def initialize(@name : String, @id : String) + end + + def system_name : String + @name + end + + def system_id : String + @id + end + + def self.from_name?(groupname : String) : ::System::Group? + if found = Crystal::System.name_to_sid(groupname) + from_sid(found.sid) + end + end + + def self.from_id?(groupid : String) : ::System::Group? + if sid = Crystal::System.sid_from_s(groupid) + begin + from_sid(sid) + ensure + LibC.LocalFree(sid) + end + end + end + + private def self.from_sid(sid : LibC::SID*) : ::System::Group? + canonical = Crystal::System.sid_to_name(sid) || return + + # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/7b2aeb27-92fc-41f6-8437-deb65d950921#gt_0387e636-5654-4910-9519-1f8326cf5ec0 + # SidTypeAlias should also be treated as a group type next to SidTypeGroup + # and SidTypeWellKnownGroup: + # "alias object -> resource group: A group object..." + # + # Tests show that "Administrators" can be considered of type SidTypeAlias. + case canonical.type + when .sid_type_group?, .sid_type_well_known_group?, .sid_type_alias? + domain_and_group = canonical.domain.empty? ? canonical.name : "#{canonical.domain}\\#{canonical.name}" + gid = Crystal::System.sid_to_s(sid) + ::System::Group.new(domain_and_group, gid) + end + end +end diff --git a/src/crystal/system/win32/user.cr b/src/crystal/system/win32/user.cr index e5fcdbba10aa..4a06570c72b8 100644 --- a/src/crystal/system/win32/user.cr +++ b/src/crystal/system/win32/user.cr @@ -1,4 +1,4 @@ -require "c/sddl" +require "crystal/system/windows" require "c/lm" require "c/userenv" require "c/security" @@ -71,7 +71,7 @@ module Crystal::System::User end def self.from_username?(username : String) : ::System::User? - if found = name_to_sid(username) + if found = Crystal::System.name_to_sid(username) if found.type.sid_type_user? from_sid(found.sid) end @@ -79,7 +79,7 @@ module Crystal::System::User end def self.from_id?(id : String) : ::System::User? - if sid = sid_from_s(id) + if sid = Crystal::System.sid_from_s(id) begin from_sid(sid) ensure @@ -89,13 +89,13 @@ module Crystal::System::User end private def self.from_sid(sid : LibC::SID*) : ::System::User? - canonical = sid_to_name(sid) || return + canonical = Crystal::System.sid_to_name(sid) || return return unless canonical.type.sid_type_user? domain_and_user = "#{canonical.domain}\\#{canonical.name}" full_name = lookup_full_name(canonical.name, canonical.domain, domain_and_user) || return pgid = lookup_primary_group_id(canonical.name, canonical.domain) || return - uid = sid_to_s(sid) + uid = Crystal::System.sid_to_s(sid) home_dir = lookup_home_directory(uid, canonical.name) || return ::System::User.new(domain_and_user, uid, pgid, full_name, home_dir) @@ -136,10 +136,10 @@ module Crystal::System::User # https://support.microsoft.com/en-us/help/297951/how-to-use-the-primarygroupid-attribute-to-find-the-primary-group-for # The method follows this formula: domainRID + "-" + primaryGroupRID private def self.lookup_primary_group_id(name : String, domain : String) : String? - domain_sid = name_to_sid(domain) || return + domain_sid = Crystal::System.name_to_sid(domain) || return return unless domain_sid.type.sid_type_domain? - domain_sid_str = sid_to_s(domain_sid.sid) + domain_sid_str = Crystal::System.sid_to_s(domain_sid.sid) # If the user has joined a domain use the RID of the default primary group # called "Domain Users": @@ -210,43 +210,6 @@ module Crystal::System::User return "#{profile_dir}\\#{username}" if profile_dir end - private record SIDLookupResult, sid : LibC::SID*, domain : String, type : LibC::SID_NAME_USE - - private def self.name_to_sid(name : String) : SIDLookupResult? - utf16_name = Crystal::System.to_wstr(name) - - sid_size = LibC::DWORD.zero - domain_buf_size = LibC::DWORD.zero - LibC.LookupAccountNameW(nil, utf16_name, nil, pointerof(sid_size), nil, pointerof(domain_buf_size), out _) - - unless WinError.value.error_none_mapped? - sid = Pointer(UInt8).malloc(sid_size).as(LibC::SID*) - domain_buf = Slice(LibC::WCHAR).new(domain_buf_size) - if LibC.LookupAccountNameW(nil, utf16_name, sid, pointerof(sid_size), domain_buf, pointerof(domain_buf_size), out sid_type) != 0 - domain = String.from_utf16(domain_buf[..-2]) - SIDLookupResult.new(sid, domain, sid_type) - end - end - end - - private record NameLookupResult, name : String, domain : String, type : LibC::SID_NAME_USE - - private def self.sid_to_name(sid : LibC::SID*) : NameLookupResult? - name_buf_size = LibC::DWORD.zero - domain_buf_size = LibC::DWORD.zero - LibC.LookupAccountSidW(nil, sid, nil, pointerof(name_buf_size), nil, pointerof(domain_buf_size), out _) - - unless WinError.value.error_none_mapped? - name_buf = Slice(LibC::WCHAR).new(name_buf_size) - domain_buf = Slice(LibC::WCHAR).new(domain_buf_size) - if LibC.LookupAccountSidW(nil, sid, name_buf, pointerof(name_buf_size), domain_buf, pointerof(domain_buf_size), out sid_type) != 0 - name = String.from_utf16(name_buf[..-2]) - domain = String.from_utf16(domain_buf[..-2]) - NameLookupResult.new(name, domain, sid_type) - end - end - end - private def self.domain_joined? : Bool status = LibC.NetGetJoinInformation(nil, out domain, out type) if status != LibC::NERR_Success @@ -256,18 +219,4 @@ module Crystal::System::User LibC.NetApiBufferFree(domain) is_domain end - - private def self.sid_to_s(sid : LibC::SID*) : String - if LibC.ConvertSidToStringSidW(sid, out ptr) == 0 - raise RuntimeError.from_winerror("ConvertSidToStringSidW") - end - str, _ = String.from_utf16(ptr) - LibC.LocalFree(ptr) - str - end - - private def self.sid_from_s(str : String) : LibC::SID* - status = LibC.ConvertStringSidToSidW(Crystal::System.to_wstr(str), out sid) - status != 0 ? sid : Pointer(LibC::SID).null - end end diff --git a/src/crystal/system/windows.cr b/src/crystal/system/windows.cr index b303d4d61f6d..90b38396cf8f 100644 --- a/src/crystal/system/windows.cr +++ b/src/crystal/system/windows.cr @@ -1,3 +1,5 @@ +require "c/sddl" + # :nodoc: module Crystal::System def self.retry_wstr_buffer(&) @@ -13,4 +15,55 @@ module Crystal::System def self.to_wstr(str : String, name : String? = nil) : LibC::LPWSTR str.check_no_null_byte(name).to_utf16.to_unsafe end + + def self.sid_to_s(sid : LibC::SID*) : String + if LibC.ConvertSidToStringSidW(sid, out ptr) == 0 + raise RuntimeError.from_winerror("ConvertSidToStringSidW") + end + str, _ = String.from_utf16(ptr) + LibC.LocalFree(ptr) + str + end + + def self.sid_from_s(str : String) : LibC::SID* + status = LibC.ConvertStringSidToSidW(to_wstr(str), out sid) + status != 0 ? sid : Pointer(LibC::SID).null + end + + record SIDLookupResult, sid : LibC::SID*, domain : String, type : LibC::SID_NAME_USE + + def self.name_to_sid(name : String) : SIDLookupResult? + utf16_name = to_wstr(name) + + sid_size = LibC::DWORD.zero + domain_buf_size = LibC::DWORD.zero + LibC.LookupAccountNameW(nil, utf16_name, nil, pointerof(sid_size), nil, pointerof(domain_buf_size), out _) + + unless WinError.value.error_none_mapped? + sid = Pointer(UInt8).malloc(sid_size).as(LibC::SID*) + domain_buf = Slice(LibC::WCHAR).new(domain_buf_size) + if LibC.LookupAccountNameW(nil, utf16_name, sid, pointerof(sid_size), domain_buf, pointerof(domain_buf_size), out sid_type) != 0 + domain = String.from_utf16(domain_buf[..-2]) + SIDLookupResult.new(sid, domain, sid_type) + end + end + end + + record NameLookupResult, name : String, domain : String, type : LibC::SID_NAME_USE + + def self.sid_to_name(sid : LibC::SID*) : NameLookupResult? + name_buf_size = LibC::DWORD.zero + domain_buf_size = LibC::DWORD.zero + LibC.LookupAccountSidW(nil, sid, nil, pointerof(name_buf_size), nil, pointerof(domain_buf_size), out _) + + unless WinError.value.error_none_mapped? + name_buf = Slice(LibC::WCHAR).new(name_buf_size) + domain_buf = Slice(LibC::WCHAR).new(domain_buf_size) + if LibC.LookupAccountSidW(nil, sid, name_buf, pointerof(name_buf_size), domain_buf, pointerof(domain_buf_size), out sid_type) != 0 + name = String.from_utf16(name_buf[..-2]) + domain = String.from_utf16(domain_buf[..-2]) + NameLookupResult.new(name, domain, sid_type) + end + end + end end diff --git a/src/docs_main.cr b/src/docs_main.cr index e670d6d3fa83..ab3ee2affdbc 100644 --- a/src/docs_main.cr +++ b/src/docs_main.cr @@ -56,8 +56,6 @@ require "./uri/params/serializable" require "./uuid" require "./uuid/json" require "./syscall" -{% unless flag?(:win32) %} - require "./system/*" -{% end %} +require "./system/*" require "./wait_group" require "./docs_pseudo_methods"