fix(auth): add Windows session storage bridge
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
// Package authstorage implements tht's hidden, stdin/stdout-only bridge for protected browser
|
||||
// session files. It deliberately has no operator-facing commands or installation configuration.
|
||||
package authstorage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
|
||||
)
|
||||
|
||||
const (
|
||||
protocolVersion = 1
|
||||
maximumProtocolBytes = 64 * 1024
|
||||
maximumSessionBytes = 16 * 1024
|
||||
maximumOIDCStateBytes = 8 * 1024
|
||||
maximumEntries = 256
|
||||
)
|
||||
|
||||
var (
|
||||
digestFilename = regexp.MustCompile(`^[a-f0-9]{64}\.json$`)
|
||||
claimFilename = regexp.MustCompile(`^[a-f0-9]{64}\.claim$`)
|
||||
errInvalid = errors.New("auth storage request invalid")
|
||||
)
|
||||
|
||||
type request struct {
|
||||
Version int `json:"version"`
|
||||
Operation string `json:"operation"`
|
||||
Root string `json:"root"`
|
||||
Directory string `json:"directory"`
|
||||
Filename string `json:"filename,omitempty"`
|
||||
ContentBase64 string `json:"contentBase64,omitempty"`
|
||||
}
|
||||
|
||||
type response struct {
|
||||
Version int `json:"version"`
|
||||
OK bool `json:"ok"`
|
||||
Created bool `json:"created,omitempty"`
|
||||
Replaced bool `json:"replaced,omitempty"`
|
||||
Removed bool `json:"removed,omitempty"`
|
||||
Found bool `json:"found,omitempty"`
|
||||
Claimed bool `json:"claimed,omitempty"`
|
||||
ContentBase64 string `json:"contentBase64,omitempty"`
|
||||
Entries []safeio.PrivateDirectoryEntry `json:"entries,omitempty"`
|
||||
}
|
||||
|
||||
// Run accepts exactly one strict JSON request on stdin and emits exactly one JSON response on
|
||||
// stdout. All diagnostic text is fixed and goes only to stderr.
|
||||
func Run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
||||
if len(args) != 0 || ctx == nil || stdin == nil || stdout == nil || stderr == nil {
|
||||
return fail(stderr)
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return fail(stderr)
|
||||
}
|
||||
decoder := json.NewDecoder(io.LimitReader(stdin, maximumProtocolBytes+1))
|
||||
decoder.DisallowUnknownFields()
|
||||
var input request
|
||||
if err := decoder.Decode(&input); err != nil {
|
||||
return fail(stderr)
|
||||
}
|
||||
var extra any
|
||||
if err := decoder.Decode(&extra); err != io.EOF {
|
||||
return fail(stderr)
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return fail(stderr)
|
||||
}
|
||||
result, err := execute(input)
|
||||
if err != nil || ctx.Err() != nil {
|
||||
return fail(stderr)
|
||||
}
|
||||
encoder := json.NewEncoder(stdout)
|
||||
encoder.SetEscapeHTML(false)
|
||||
if err := encoder.Encode(result); err != nil {
|
||||
return fail(stderr)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func fail(stderr io.Writer) int {
|
||||
_, _ = io.WriteString(stderr, "tht: auth storage request failed\n")
|
||||
return 1
|
||||
}
|
||||
|
||||
func execute(input request) (response, error) {
|
||||
if input.Version != protocolVersion || !validDirectory(input.Directory) {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
directory, err := storageDirectory(input.Root, input.Directory)
|
||||
if err != nil {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
switch input.Operation {
|
||||
case "create":
|
||||
contents, err := decodeContents(input)
|
||||
if err != nil || !digestFilename.MatchString(input.Filename) {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
created, err := createPrivate(directory, input.Filename, contents)
|
||||
if err != nil {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
return response{Version: protocolVersion, OK: true, Created: created}, nil
|
||||
case "read":
|
||||
if !digestFilename.MatchString(input.Filename) {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
contents, found, err := readPrivate(directory, input.Filename, recordMaximum(input.Directory))
|
||||
if err != nil {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
return contentResponse(found, contents), nil
|
||||
case "replace":
|
||||
contents, err := decodeContents(input)
|
||||
if err != nil || !digestFilename.MatchString(input.Filename) {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
if err := safeio.ReplaceCanonicalRegular(filepath.Join(directory, input.Filename), contents, 0o600); err != nil {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
return response{Version: protocolVersion, OK: true, Replaced: true}, nil
|
||||
case "remove":
|
||||
if !digestFilename.MatchString(input.Filename) && !claimFilename.MatchString(input.Filename) {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
removed, err := removePrivate(directory, input.Filename)
|
||||
if err != nil {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
return response{Version: protocolVersion, OK: true, Removed: removed}, nil
|
||||
case "list":
|
||||
entries, err := safeio.ListCanonicalPrivateDirectory(directory, maximumEntries)
|
||||
if err != nil {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if !digestFilename.MatchString(entry.Name) && !claimFilename.MatchString(entry.Name) {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
}
|
||||
return response{Version: protocolVersion, OK: true, Entries: entries}, nil
|
||||
case "claim-consume":
|
||||
if input.Directory != "oidc" || !digestFilename.MatchString(input.Filename) {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
return claimConsume(directory, input.Filename)
|
||||
case "read-claim":
|
||||
if input.Directory != "oidc" || !digestFilename.MatchString(input.Filename) {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
contents, found, err := safeio.ReadCanonicalPrivateClaim(
|
||||
filepath.Join(directory, input.Filename),
|
||||
filepath.Join(directory, asClaimFilename(input.Filename)),
|
||||
maximumOIDCStateBytes,
|
||||
)
|
||||
if err != nil {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
return contentResponse(found, contents), nil
|
||||
case "remove-claim":
|
||||
if input.Directory != "oidc" || !digestFilename.MatchString(input.Filename) {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
removed, err := safeio.RemoveCanonicalPrivateClaim(
|
||||
filepath.Join(directory, input.Filename),
|
||||
filepath.Join(directory, asClaimFilename(input.Filename)),
|
||||
)
|
||||
if err != nil {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
return response{Version: protocolVersion, OK: true, Removed: removed}, nil
|
||||
default:
|
||||
return response{}, errInvalid
|
||||
}
|
||||
}
|
||||
|
||||
func contentResponse(found bool, contents []byte) response {
|
||||
if !found {
|
||||
return response{Version: protocolVersion, OK: true}
|
||||
}
|
||||
return response{Version: protocolVersion, OK: true, Found: true, ContentBase64: base64.StdEncoding.EncodeToString(contents)}
|
||||
}
|
||||
|
||||
func storageDirectory(root, directory string) (string, error) {
|
||||
if !filepath.IsAbs(root) || filepath.Clean(root) != root || strings.ContainsRune(root, '\x00') || safeio.EnsurePrivateDirectory(root) != nil {
|
||||
return "", errInvalid
|
||||
}
|
||||
path := filepath.Join(root, directory)
|
||||
if filepath.Dir(path) != root || safeio.EnsurePrivateDirectory(path) != nil {
|
||||
return "", errInvalid
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func validDirectory(value string) bool {
|
||||
return value == "sessions" || value == "oidc"
|
||||
}
|
||||
|
||||
func recordMaximum(directory string) int64 {
|
||||
if directory == "oidc" {
|
||||
return maximumOIDCStateBytes
|
||||
}
|
||||
return maximumSessionBytes
|
||||
}
|
||||
|
||||
func decodeContents(input request) ([]byte, error) {
|
||||
contents, err := base64.StdEncoding.DecodeString(input.ContentBase64)
|
||||
if err != nil || len(contents) == 0 || int64(len(contents)) > recordMaximum(input.Directory) {
|
||||
return nil, errInvalid
|
||||
}
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
func createPrivate(directory, filename string, contents []byte) (bool, error) {
|
||||
path := filepath.Join(directory, filename)
|
||||
if _, err := os.Lstat(path); err == nil {
|
||||
if safeio.ValidatePrivateRegular(path) != nil {
|
||||
return false, errInvalid
|
||||
}
|
||||
return false, nil
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return false, errInvalid
|
||||
}
|
||||
if err := safeio.WriteCanonicalNewFile(path, contents, 0o600); err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if safeio.ValidatePrivateRegular(path) == nil {
|
||||
return false, nil
|
||||
}
|
||||
return false, errInvalid
|
||||
}
|
||||
|
||||
func readPrivate(directory, filename string, maximum int64) ([]byte, bool, error) {
|
||||
path := filepath.Join(directory, filename)
|
||||
if _, err := os.Lstat(path); errors.Is(err, os.ErrNotExist) {
|
||||
return nil, false, nil
|
||||
} else if err != nil {
|
||||
return nil, false, errInvalid
|
||||
}
|
||||
contents, err := safeio.ReadCanonicalPrivateRegular(path, maximum)
|
||||
if err != nil {
|
||||
return nil, false, errInvalid
|
||||
}
|
||||
return contents, true, nil
|
||||
}
|
||||
|
||||
func removePrivate(directory, filename string) (bool, error) {
|
||||
path := filepath.Join(directory, filename)
|
||||
if _, err := os.Lstat(path); errors.Is(err, os.ErrNotExist) {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
return false, errInvalid
|
||||
}
|
||||
if err := safeio.RemoveCanonicalPrivateRegular(path); err != nil {
|
||||
return false, errInvalid
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func claimConsume(directory, filename string) (response, error) {
|
||||
source := filepath.Join(directory, filename)
|
||||
claim := filepath.Join(directory, asClaimFilename(filename))
|
||||
claimed, err := safeio.ClaimCanonicalPrivateRegular(source, claim)
|
||||
if err != nil {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
if !claimed {
|
||||
return response{Version: protocolVersion, OK: true}, nil
|
||||
}
|
||||
contents, found, err := safeio.ReadCanonicalPrivateClaim(source, claim, maximumOIDCStateBytes)
|
||||
if err != nil || !found {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
removed, err := safeio.RemoveCanonicalPrivateClaim(source, claim)
|
||||
if err != nil || !removed {
|
||||
return response{}, errInvalid
|
||||
}
|
||||
return contentResponse(true, contents), nil
|
||||
}
|
||||
|
||||
func asClaimFilename(filename string) string {
|
||||
return strings.TrimSuffix(filename, ".json") + ".claim"
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package authstorage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/testsupport"
|
||||
)
|
||||
|
||||
func TestProtocolCreatesReadsReplacesListsAndRemovesPrivateRecord(t *testing.T) {
|
||||
root := filepath.Join(privateTestRoot(t), "auth")
|
||||
filename := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.json"
|
||||
|
||||
created := runRequest(t, request{Version: 1, Operation: "create", Root: root, Directory: "sessions", Filename: filename, ContentBase64: base64.StdEncoding.EncodeToString([]byte("first"))})
|
||||
if !created.Created {
|
||||
t.Fatal("create did not report a new record")
|
||||
}
|
||||
if duplicate := runRequest(t, request{Version: 1, Operation: "create", Root: root, Directory: "sessions", Filename: filename, ContentBase64: base64.StdEncoding.EncodeToString([]byte("other"))}); duplicate.Created {
|
||||
t.Fatal("duplicate exclusive create reported success")
|
||||
}
|
||||
read := runRequest(t, request{Version: 1, Operation: "read", Root: root, Directory: "sessions", Filename: filename})
|
||||
if !read.Found || decodeContent(t, read) != "first" {
|
||||
t.Fatalf("read = %#v, want private first record", read)
|
||||
}
|
||||
updated := runRequest(t, request{Version: 1, Operation: "replace", Root: root, Directory: "sessions", Filename: filename, ContentBase64: base64.StdEncoding.EncodeToString([]byte("second"))})
|
||||
if !updated.Replaced {
|
||||
t.Fatal("replace did not report success")
|
||||
}
|
||||
listed := runRequest(t, request{Version: 1, Operation: "list", Root: root, Directory: "sessions"})
|
||||
if len(listed.Entries) != 1 || listed.Entries[0].Name != filename {
|
||||
t.Fatalf("list = %#v, want exactly %q", listed.Entries, filename)
|
||||
}
|
||||
removed := runRequest(t, request{Version: 1, Operation: "remove", Root: root, Directory: "sessions", Filename: filename})
|
||||
if !removed.Removed {
|
||||
t.Fatal("remove did not report success")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtocolClaimConsumeIsAtomicAcrossConcurrentRequests(t *testing.T) {
|
||||
root := filepath.Join(privateTestRoot(t), "auth")
|
||||
filename := "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd.json"
|
||||
runRequest(t, request{Version: 1, Operation: "create", Root: root, Directory: "oidc", Filename: filename, ContentBase64: base64.StdEncoding.EncodeToString([]byte("oidc-record"))})
|
||||
|
||||
request := request{Version: 1, Operation: "claim-consume", Root: root, Directory: "oidc", Filename: filename}
|
||||
results := make(chan response, 2)
|
||||
errors := make(chan error, 2)
|
||||
var group sync.WaitGroup
|
||||
for range 2 {
|
||||
group.Add(1)
|
||||
go func() {
|
||||
defer group.Done()
|
||||
result, err := execute(request)
|
||||
if err != nil {
|
||||
errors <- err
|
||||
return
|
||||
}
|
||||
results <- result
|
||||
}()
|
||||
}
|
||||
group.Wait()
|
||||
close(results)
|
||||
close(errors)
|
||||
for err := range errors {
|
||||
t.Fatalf("concurrent claim error = %v", err)
|
||||
}
|
||||
found := 0
|
||||
for result := range results {
|
||||
if result.Found {
|
||||
found++
|
||||
}
|
||||
}
|
||||
if found != 1 {
|
||||
t.Fatalf("winning claim count = %d, want 1", found)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtocolRejectsBoundsReparseAndUnexpectedStorageNames(t *testing.T) {
|
||||
root := filepath.Join(privateTestRoot(t), "auth")
|
||||
filename := "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.json"
|
||||
runRejected(t, request{Version: 1, Operation: "create", Root: root, Directory: "sessions", Filename: filename, ContentBase64: base64.StdEncoding.EncodeToString(make([]byte, maximumSessionBytes+1))})
|
||||
|
||||
valid := request{Version: 1, Operation: "create", Root: root, Directory: "sessions", Filename: filename, ContentBase64: base64.StdEncoding.EncodeToString([]byte("record"))}
|
||||
runRequest(t, valid)
|
||||
if err := safeio.WriteCanonicalNewFile(filepath.Join(root, "sessions", "unexpected.txt"), []byte("junk"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runRejected(t, request{Version: 1, Operation: "list", Root: root, Directory: "sessions"})
|
||||
|
||||
outer := privateTestRoot(t)
|
||||
linkedRoot := filepath.Join(outer, "linked-auth")
|
||||
testsupport.SymlinkOrSkip(t, filepath.Join(outer, "missing-real-auth"), linkedRoot)
|
||||
runRejected(t, request{Version: 1, Operation: "create", Root: linkedRoot, Directory: "sessions", Filename: filename, ContentBase64: base64.StdEncoding.EncodeToString([]byte("record"))})
|
||||
}
|
||||
|
||||
func TestProtocolClaimConsumeHasOneWinner(t *testing.T) {
|
||||
root := filepath.Join(privateTestRoot(t), "auth")
|
||||
filename := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.json"
|
||||
runRequest(t, request{Version: 1, Operation: "create", Root: root, Directory: "oidc", Filename: filename, ContentBase64: base64.StdEncoding.EncodeToString([]byte("oidc-record"))})
|
||||
|
||||
first := runRequest(t, request{Version: 1, Operation: "claim-consume", Root: root, Directory: "oidc", Filename: filename})
|
||||
second := runRequest(t, request{Version: 1, Operation: "claim-consume", Root: root, Directory: "oidc", Filename: filename})
|
||||
if !first.Found || decodeContent(t, first) != "oidc-record" || second.Found {
|
||||
t.Fatalf("claim results = %#v, %#v", first, second)
|
||||
}
|
||||
}
|
||||
|
||||
func runRequest(t *testing.T, value request) response {
|
||||
t.Helper()
|
||||
input, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var stdout, stderr bytes.Buffer
|
||||
if code := Run(context.Background(), nil, bytes.NewReader(input), &stdout, &stderr); code != 0 {
|
||||
t.Fatalf("Run() code = %d stderr = %q", code, stderr.String())
|
||||
}
|
||||
var output response
|
||||
if err := json.Unmarshal(stdout.Bytes(), &output); err != nil || !output.OK || output.Version != 1 {
|
||||
t.Fatalf("stdout = %q response = %#v error = %v", stdout.String(), output, err)
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func runRejected(t *testing.T, value request) {
|
||||
t.Helper()
|
||||
input, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var stdout, stderr bytes.Buffer
|
||||
if code := Run(context.Background(), nil, bytes.NewReader(input), &stdout, &stderr); code == 0 || stdout.Len() != 0 || stderr.String() != "tht: auth storage request failed\n" {
|
||||
t.Fatalf("Run() rejection code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func decodeContent(t *testing.T, value response) string {
|
||||
t.Helper()
|
||||
contents, err := base64.StdEncoding.DecodeString(value.ContentBase64)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(contents)
|
||||
}
|
||||
|
||||
func privateTestRoot(t *testing.T) string {
|
||||
t.Helper()
|
||||
temporary, err := filepath.EvalSymlinks(os.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root, err := os.MkdirTemp(temporary, "tht-authstorage-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.RemoveAll(root) })
|
||||
return root
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
//go:build windows
|
||||
|
||||
package authstorage
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func TestProtocolCreatesRecordsWithOwnerOnlyDACLAndRejectsReparseRoot(t *testing.T) {
|
||||
root := filepath.Join(t.TempDir(), "auth")
|
||||
filename := "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc.json"
|
||||
runRequest(t, request{Version: 1, Operation: "create", Root: root, Directory: "sessions", Filename: filename, ContentBase64: base64.StdEncoding.EncodeToString([]byte("private"))})
|
||||
if err := safeio.ValidatePrivateDirectory(root); err != nil {
|
||||
t.Fatalf("root DACL = %v", err)
|
||||
}
|
||||
if err := safeio.ValidatePrivateDirectory(filepath.Join(root, "sessions")); err != nil {
|
||||
t.Fatalf("sessions DACL = %v", err)
|
||||
}
|
||||
if err := safeio.ValidatePrivateRegular(filepath.Join(root, "sessions", filename)); err != nil {
|
||||
t.Fatalf("record DACL = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtocolRejectsPermissiveDACLAndReparseRoot(t *testing.T) {
|
||||
root := filepath.Join(t.TempDir(), "auth")
|
||||
filename := "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff.json"
|
||||
runRequest(t, request{Version: 1, Operation: "create", Root: root, Directory: "sessions", Filename: filename, ContentBase64: base64.StdEncoding.EncodeToString([]byte("private"))})
|
||||
path := filepath.Join(root, "sessions", filename)
|
||||
if err := setPermissiveDACL(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runRejected(t, request{Version: 1, Operation: "read", Root: root, Directory: "sessions", Filename: filename})
|
||||
|
||||
linkedRoot := filepath.Join(t.TempDir(), "reparse-auth")
|
||||
if err := os.Symlink(root, linkedRoot); err != nil {
|
||||
t.Skipf("Windows host does not permit test symlink creation: %v", err)
|
||||
}
|
||||
runRejected(t, request{Version: 1, Operation: "create", Root: linkedRoot, Directory: "sessions", Filename: filename, ContentBase64: base64.StdEncoding.EncodeToString([]byte("private"))})
|
||||
}
|
||||
|
||||
func setPermissiveDACL(path string) error {
|
||||
world, err := windows.StringToSid("S-1-1-0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var pinner runtime.Pinner
|
||||
pinner.Pin(world)
|
||||
defer pinner.Unpin()
|
||||
acl, err := windows.ACLFromEntries([]windows.EXPLICIT_ACCESS{{
|
||||
AccessPermissions: windows.GENERIC_READ | windows.GENERIC_WRITE,
|
||||
AccessMode: windows.GRANT_ACCESS,
|
||||
Trustee: windows.TRUSTEE{
|
||||
TrusteeForm: windows.TRUSTEE_IS_SID,
|
||||
TrusteeType: windows.TRUSTEE_IS_GROUP,
|
||||
TrusteeValue: windows.TrusteeValueFromSID(world),
|
||||
},
|
||||
}}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return windows.SetNamedSecurityInfo(path, windows.SE_FILE_OBJECT,
|
||||
windows.DACL_SECURITY_INFORMATION|windows.PROTECTED_DACL_SECURITY_INFORMATION,
|
||||
nil, nil, acl, nil)
|
||||
}
|
||||
Reference in New Issue
Block a user