diff --git a/internal/aghos/permission.go b/internal/aghos/permission.go index e0e99400..24fd70bc 100644 --- a/internal/aghos/permission.go +++ b/internal/aghos/permission.go @@ -18,6 +18,12 @@ func Mkdir(name string, perm fs.FileMode) (err error) { return mkdir(name, perm) } -// TODO(e.burkov): !! add Stat. +// Stat is an extension for [os.Stat] that properly handles Windows access +// rights. +// +// TODO(e.burkov): !! use. +func Stat(name string) (fi fs.FileInfo, err error) { + return stat(name) +} // TODO(e.burkov): !! add tests. diff --git a/internal/aghos/permission_unix.go b/internal/aghos/permission_unix.go index 62266d4e..b9891c29 100644 --- a/internal/aghos/permission_unix.go +++ b/internal/aghos/permission_unix.go @@ -1,4 +1,4 @@ -//go:build !windows +//go:build unix package aghos @@ -16,3 +16,8 @@ func chmod(name string, perm fs.FileMode) (err error) { func mkdir(name string, perm fs.FileMode) (err error) { return os.Mkdir(name, perm) } + +// stat is a Unix implementation of [Stat]. +func stat(name string) (fi os.FileInfo, err error) { + return os.Stat(name) +} diff --git a/internal/aghos/permission_windows.go b/internal/aghos/permission_windows.go index 099e633f..3fcd6314 100644 --- a/internal/aghos/permission_windows.go +++ b/internal/aghos/permission_windows.go @@ -9,64 +9,134 @@ import ( "path/filepath" "unsafe" + "github.com/AdguardTeam/golibs/container" + "github.com/AdguardTeam/golibs/errors" "golang.org/x/sys/windows" ) +// fileInfo is a Windows implementation of [fs.FileInfo], that contains the +// filemode converted from the security descriptor. +type fileInfo struct { + // fs.FileInfo is embedded to provide the default implementations and info + // successfully retrieved by [os.Stat]. + fs.FileInfo + + // mode is the file mode converted from the security descriptor. + mode fs.FileMode +} + +// type check +var _ fs.FileInfo = (*fileInfo)(nil) + +// Mode implements [fs.FileInfo.Mode] for [*fileInfo]. +func (fi *fileInfo) Mode() (mode fs.FileMode) { return fi.mode } + +// stat is a Windows implementation of [Stat]. +func stat(name string) (fi os.FileInfo, err error) { + fi, err = os.Stat(name) + if err != nil { + return nil, err + } + + const objectType windows.SE_OBJECT_TYPE = windows.SE_FILE_OBJECT + + secInfo := windows.SECURITY_INFORMATION(0 | + windows.OWNER_SECURITY_INFORMATION | + windows.GROUP_SECURITY_INFORMATION | + windows.DACL_SECURITY_INFORMATION | + windows.PROTECTED_DACL_SECURITY_INFORMATION, + ) + + sd, err := windows.GetNamedSecurityInfo(fi.Name(), objectType, secInfo) + if err != nil { + return nil, fmt.Errorf("getting security descriptor: %w", err) + } + + dacl, _, err := sd.DACL() + if err != nil { + return nil, fmt.Errorf("getting discretionary access control list: %w", err) + } + + owner, _, err := sd.Owner() + if err != nil { + return nil, fmt.Errorf("getting owner sid: %w", err) + } + + group, _, err := sd.Group() + if err != nil { + return nil, fmt.Errorf("getting group sid: %w", err) + } + + var ownerMask, groupMask, otherMask windows.ACCESS_MASK + for i := range uint32(dacl.AceCount) { + var ace *windows.ACCESS_ALLOWED_ACE + err = windows.GetAce(dacl, i, &ace) + if err != nil { + return nil, fmt.Errorf("getting access control entry at index %d: %w", i, err) + } + + entrySid := (*windows.SID)(unsafe.Pointer(&ace.SidStart)) + switch { + case entrySid.Equals(owner): + ownerMask |= ace.Mask + case entrySid.Equals(group): + groupMask |= ace.Mask + default: + otherMask = ace.Mask + } + } + + return &fileInfo{ + FileInfo: fi, + mode: masksToPerm(ownerMask, groupMask, otherMask), + }, nil +} + // chmod is a Windows implementation of [Chmod]. func chmod(name string, perm fs.FileMode) (err error) { const objectType windows.SE_OBJECT_TYPE = windows.SE_FILE_OBJECT entries := make([]windows.EXPLICIT_ACCESS, 0, 3) - creatorMask, groupMask, worldMask := modeToMasks(perm.Perm()) + creatorMask, groupMask, worldMask := modeToMasks(perm) - if creatorMask > 0 { - var creator *windows.TRUSTEE - creator, err = newWellKnownTrustee(windows.WinCreatorOwnerSid) + sidMasks := container.KeyValues[windows.WELL_KNOWN_SID_TYPE, windows.ACCESS_MASK]{{ + Key: windows.WinCreatorOwnerSid, + Value: creatorMask, + }, { + Key: windows.WinCreatorGroupSid, + Value: groupMask, + }, { + Key: windows.WinWorldSid, + Value: worldMask, + }} + + var errs []error + for _, sidMask := range sidMasks { + if sidMask.Value == 0 { + continue + } + + var trustee *windows.TRUSTEE + trustee, err = newWellKnownTrustee(sidMask.Key) if err != nil { - return fmt.Errorf("creating owner trustee: %w", err) + errs = append(errs, err) } entries = append(entries, windows.EXPLICIT_ACCESS{ - AccessPermissions: creatorMask, - AccessMode: windows.SET_ACCESS, + AccessPermissions: sidMask.Value, + AccessMode: windows.GRANT_ACCESS, Inheritance: windows.NO_INHERITANCE, - Trustee: *creator, + Trustee: *trustee, }) } - if groupMask > 0 { - var group *windows.TRUSTEE - group, err = newWellKnownTrustee(windows.WinCreatorGroupSid) - if err != nil { - return fmt.Errorf("creating group trustee: %w", err) - } - - entries = append(entries, windows.EXPLICIT_ACCESS{ - AccessPermissions: groupMask, - AccessMode: windows.SET_ACCESS, - Inheritance: windows.NO_INHERITANCE, - Trustee: *group, - }) - } - - if worldMask > 0 { - var world *windows.TRUSTEE - world, err = newWellKnownTrustee(windows.WinWorldSid) - if err != nil { - return fmt.Errorf("creating everyone trustee: %w", err) - } - - entries = append(entries, windows.EXPLICIT_ACCESS{ - AccessPermissions: worldMask, - AccessMode: windows.SET_ACCESS, - Inheritance: windows.NO_INHERITANCE, - Trustee: *world, - }) + if err = errors.Join(errs...); err != nil { + return fmt.Errorf("creating access control entries: %w", err) } acl, err := windows.ACLFromEntries(entries, nil) if err != nil { - return fmt.Errorf("creating acl: %w", err) + return fmt.Errorf("creating access control list: %w", err) } secInfo := windows.SECURITY_INFORMATION( @@ -75,164 +145,41 @@ func chmod(name string, perm fs.FileMode) (err error) { err = windows.SetNamedSecurityInfo(name, objectType, secInfo, nil, nil, acl, nil) if err != nil { - return fmt.Errorf("setting security information: %w", err) + return fmt.Errorf("setting security descriptor: %w", err) } return nil } // mkdir is a Windows implementation of [Mkdir]. +// +// TODO(e.burkov): Consider using [windows.CreateDirectory] instead of +// [os.Mkdir] to reduce the number of syscalls. func mkdir(name string, perm os.FileMode) (err error) { name, err = filepath.Abs(name) if err != nil { return fmt.Errorf("computing absolute path: %w", err) } - entries := make([]windows.EXPLICIT_ACCESS, 0, 3) - creatorMask, groupMask, worldMask := modeToMasks(perm.Perm()) + err = os.Mkdir(name, perm) + if err != nil { + return fmt.Errorf("creating directory: %w", err) + } - if creatorMask > 0 { - var creator *windows.TRUSTEE - creator, err = currentUserTrustee() + defer func() { if err != nil { - return fmt.Errorf("creating owner trustee: %w", err) + err = errors.WithDeferred(err, os.Remove(name)) } + }() - entries = append(entries, windows.EXPLICIT_ACCESS{ - AccessPermissions: creatorMask, - AccessMode: windows.GRANT_ACCESS, - Inheritance: windows.NO_INHERITANCE, - Trustee: *creator, - }) - } - - if groupMask > 0 { - var group *windows.TRUSTEE - group, err = currentUserGroupTrustee() - if err != nil { - return fmt.Errorf("creating group trustee: %w", err) - } - - entries = append(entries, windows.EXPLICIT_ACCESS{ - AccessPermissions: groupMask, - AccessMode: windows.GRANT_ACCESS, - Inheritance: windows.NO_INHERITANCE, - Trustee: *group, - }) - } - - if worldMask > 0 { - var world *windows.TRUSTEE - world, err = newWellKnownTrustee(windows.WinWorldSid) - if err != nil { - return fmt.Errorf("creating everyone trustee: %w", err) - } - - entries = append(entries, windows.EXPLICIT_ACCESS{ - AccessPermissions: worldMask, - AccessMode: windows.GRANT_ACCESS, - Inheritance: windows.NO_INHERITANCE, - Trustee: *world, - }) - } - - secAttrs, err := newSecAttr(entries) - if err != nil { - return fmt.Errorf("creating security attributes: %w", err) - } - - namePntr, err := windows.UTF16PtrFromString(name) - if err != nil { - return fmt.Errorf("converting string: %w", err) - } - - return windows.CreateDirectory(namePntr, secAttrs) -} - -// newSecAttr creates a new security attributes structure with the specified -// explicit access entries. -func newSecAttr(entries []windows.EXPLICIT_ACCESS) (sa *windows.SecurityAttributes, err error) { - sd, err := windows.NewSecurityDescriptor() - if err != nil { - return nil, fmt.Errorf("failed to create security descriptor: %v", err) - } - - if len(entries) > 0 { - var acl *windows.ACL - acl, err = windows.ACLFromEntries(entries, nil) - if err != nil { - return nil, fmt.Errorf("failed to create ACL from explicit access entries: %v", err) - } - - err = sd.SetDACL(acl, true, false) - if err != nil { - return nil, fmt.Errorf("failed to configure DACL for security desctriptor: %v", err) - } - } - - err = sd.SetControl(windows.SE_DACL_PROTECTED, windows.SE_DACL_PROTECTED) - if err != nil { - return nil, fmt.Errorf("failed to configure protected DACL for security descriptor: %v", err) - } - - return &windows.SecurityAttributes{ - Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})), - SecurityDescriptor: sd, - InheritHandle: 0, - }, nil -} - -// currentUserTrustee returns a trustee for the current user. -func currentUserTrustee() (t *windows.TRUSTEE, err error) { - token := windows.GetCurrentProcessToken() - tokenUser, err := token.GetTokenUser() - if err != nil { - return nil, err - } - - admins, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid) - if err != nil { - // Don't wrap the error here, as we can't add any additional context. - return nil, err - } - - sid := tokenUser.User.Sid - trusteeType := windows.TRUSTEE_TYPE(windows.TRUSTEE_IS_USER) - // TODO(e.burkov): !! consider using IsElevated() - if ok, err := token.IsMember(admins); err == nil && ok { - sid = admins - trusteeType = windows.TRUSTEE_IS_GROUP - } - - return &windows.TRUSTEE{ - TrusteeForm: windows.TRUSTEE_IS_SID, - TrusteeType: trusteeType, - TrusteeValue: windows.TrusteeValueFromSID(sid), - }, nil -} - -// currentUserGroupTrustee returns a trustee for the current user's primary -// group. -func currentUserGroupTrustee() (t *windows.TRUSTEE, err error) { - token := windows.GetCurrentProcessToken() - group, err := token.GetTokenPrimaryGroup() - if err != nil { - return nil, err - } - - return &windows.TRUSTEE{ - TrusteeForm: windows.TRUSTEE_IS_SID, - TrusteeType: windows.TRUSTEE_IS_GROUP, - TrusteeValue: windows.TrusteeValueFromSID(group.PrimaryGroup), - }, nil + return chmod(name, perm) } // newWellKnownTrustee returns a trustee for a well-known SID. func newWellKnownTrustee(stype windows.WELL_KNOWN_SID_TYPE) (t *windows.TRUSTEE, err error) { sid, err := windows.CreateWellKnownSid(stype) if err != nil { - // Don't wrap the error here, as we can't add any additional context. - return nil, err + return nil, fmt.Errorf("creating sid for type %d: %w", stype, err) } return &windows.TRUSTEE{ @@ -241,43 +188,52 @@ func newWellKnownTrustee(stype windows.WELL_KNOWN_SID_TYPE) (t *windows.TRUSTEE, }, nil } +// Constants reflecting the UNIX permission bits. +const ( + ownerWrite = 0b010000000 + groupWrite = 0b000100000 + worldWrite = 0b000000100 + + ownerAll = 0b111000000 + groupAll = 0b000111000 + worldAll = 0b000000111 +) + +// Constants reflecting the number of bits to shift the UNIX permission bits to +// convert them to the generic access rights used by Windows, see +// https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/access-mask. +const ( + genericOwner = 23 + genericGroup = 26 + genericWorld = 29 +) + +// Constants reflecting the number of bits to shift the UNIX write permission +// bits to convert them to the delete access rights used by Windows. +const ( + deleteOwner = 9 + deleteGroup = 12 + deleteWorld = 15 +) + // modeToMasks converts a UNIX file mode to the corresponding Windows access // masks. func modeToMasks(fm os.FileMode) (owner, group, world windows.ACCESS_MASK) { mask := windows.ACCESS_MASK(fm.Perm()) - // Constants reflecting the UNIX permission bits. - const ( - ownerWrite = 0o200 - groupWrite = 0o020 - worldWrite = 0o002 - - ownerAll = 0o700 - groupAll = 0o070 - worldAll = 0o007 - ) - - // Constants reflecting the number of bits to shift the UNIX permission bits - // to convert them to the generic access rights used by Windows, see - // https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/access-mask. - const ( - genericOwner = 23 - genericGroup = 26 - genericWorld = 29 - ) - - // Constants reflecting the number of bits to shift the UNIX write - // permission bits to convert them to the delete access rights used by - // Windows. - const ( - deleteOwner = 9 - deleteGroup = 12 - deleteWorld = 15 - ) - owner = ((mask & ownerAll) << genericOwner) | ((mask & ownerWrite) << deleteOwner) group = ((mask & groupAll) << genericGroup) | ((mask & groupWrite) << deleteGroup) world = ((mask & worldAll) << genericWorld) | ((mask & worldWrite) << deleteWorld) return owner, group, world } + +// masksToPerm converts Windows access masks to the corresponding UNIX file +// mode. +func masksToPerm(u, g, o windows.ACCESS_MASK) (perm os.FileMode) { + perm |= os.FileMode(((u >> genericOwner) & ownerAll) | ((u >> deleteOwner) & ownerWrite)) + perm |= os.FileMode(((g >> genericGroup) & groupAll) | ((g >> deleteGroup) & groupWrite)) + perm |= os.FileMode(((o >> genericWorld) & worldAll) | ((o >> deleteWorld) & worldWrite)) + + return perm +} diff --git a/scripts/make/go-lint.sh b/scripts/make/go-lint.sh index af0f4620..50db56d6 100644 --- a/scripts/make/go-lint.sh +++ b/scripts/make/go-lint.sh @@ -52,7 +52,8 @@ set -f -u # # * Package sort is replaced by package slices. # -# * Package unsafe is… unsafe. +# * Package unsafe is… unsafe. The only exception is Windows-specific code +# due to golang.org/x/sys/windows API design. # # Currently, the only standard exception are files generated from protobuf # schemas, which use package reflect. If your project needs more exceptions,