all: implement windows permcheck
This commit is contained in:
parent
b75ed7d4d3
commit
09e4ae1ba1
|
@ -689,7 +689,7 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
|
|||
}
|
||||
|
||||
if !opts.noPermCheck {
|
||||
checkPermissions(Context.workDir, confPath, dataDir, statsDir, querylogDir)
|
||||
checkPermissions(ctx, slogLogger, Context.workDir, confPath, dataDir, statsDir, querylogDir)
|
||||
}
|
||||
|
||||
Context.web.start()
|
||||
|
@ -751,12 +751,22 @@ func newUpdater(
|
|||
|
||||
// checkPermissions checks and migrates permissions of the files and directories
|
||||
// used by AdGuard Home, if needed.
|
||||
func checkPermissions(workDir, confPath, dataDir, statsDir, querylogDir string) {
|
||||
if permcheck.NeedsMigration(confPath) {
|
||||
permcheck.Migrate(workDir, dataDir, statsDir, querylogDir, confPath)
|
||||
func checkPermissions(
|
||||
ctx context.Context,
|
||||
baseLogger *slog.Logger,
|
||||
workDir string,
|
||||
confPath string,
|
||||
dataDir string,
|
||||
statsDir string,
|
||||
querylogDir string,
|
||||
) {
|
||||
l := baseLogger.With(slogutil.KeyPrefix, "permcheck")
|
||||
|
||||
if permcheck.NeedsMigration(ctx, l, workDir, confPath) {
|
||||
permcheck.Migrate(ctx, l, workDir, dataDir, statsDir, querylogDir, confPath)
|
||||
}
|
||||
|
||||
permcheck.Check(workDir, dataDir, statsDir, querylogDir, confPath)
|
||||
permcheck.Check(ctx, l, workDir, dataDir, statsDir, querylogDir, confPath)
|
||||
}
|
||||
|
||||
// initUsers initializes context auth module. Clears config users field.
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
//go:build unix
|
||||
|
||||
package permcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
)
|
||||
|
||||
// check is the Unix-specific implementation of [Check].
|
||||
func check(
|
||||
ctx context.Context,
|
||||
l *slog.Logger,
|
||||
workDir string,
|
||||
dataDir string,
|
||||
statsDir string,
|
||||
querylogDir string,
|
||||
confFilePath string,
|
||||
) {
|
||||
checkDir(ctx, l, workDir)
|
||||
|
||||
checkFile(ctx, l, confFilePath)
|
||||
|
||||
// TODO(a.garipov): Put all paths in one place and remove this duplication.
|
||||
checkDir(ctx, l, dataDir)
|
||||
checkDir(ctx, l, filepath.Join(dataDir, "filters"))
|
||||
checkFile(ctx, l, filepath.Join(dataDir, "sessions.db"))
|
||||
checkFile(ctx, l, filepath.Join(dataDir, "leases.json"))
|
||||
|
||||
if dataDir != querylogDir {
|
||||
checkDir(ctx, l, querylogDir)
|
||||
}
|
||||
checkFile(ctx, l, filepath.Join(querylogDir, "querylog.json"))
|
||||
checkFile(ctx, l, filepath.Join(querylogDir, "querylog.json.1"))
|
||||
|
||||
if dataDir != statsDir {
|
||||
checkDir(ctx, l, statsDir)
|
||||
}
|
||||
checkFile(ctx, l, filepath.Join(statsDir, "stats.db"))
|
||||
}
|
||||
|
||||
// checkDir checks the permissions of a single directory. The results are
|
||||
// logged at the appropriate level.
|
||||
func checkDir(ctx context.Context, l *slog.Logger, dirPath string) {
|
||||
checkPath(ctx, l, dirPath, typeDir, aghos.DefaultPermDir)
|
||||
}
|
||||
|
||||
// checkFile checks the permissions of a single file. The results are logged at
|
||||
// the appropriate level.
|
||||
func checkFile(ctx context.Context, l *slog.Logger, filePath string) {
|
||||
checkPath(ctx, l, filePath, typeFile, aghos.DefaultPermFile)
|
||||
}
|
||||
|
||||
// checkPath checks the permissions of a single filesystem entity. The results
|
||||
// are logged at the appropriate level.
|
||||
func checkPath(ctx context.Context, l *slog.Logger, entPath, fileType string, want fs.FileMode) {
|
||||
s, err := os.Stat(entPath)
|
||||
if err != nil {
|
||||
logFunc := l.ErrorContext
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
logFunc = l.DebugContext
|
||||
}
|
||||
|
||||
logFunc(
|
||||
ctx,
|
||||
"checking permissions",
|
||||
"type", fileType,
|
||||
"path", entPath,
|
||||
slogutil.KeyError, err,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Add a more fine-grained check and result reporting.
|
||||
perm := s.Mode().Perm()
|
||||
if perm != want {
|
||||
l.WarnContext(
|
||||
ctx,
|
||||
"found unexpected permissions",
|
||||
"type", fileType,
|
||||
"path", entPath,
|
||||
"got", fmt.Sprintf("%#o", perm),
|
||||
"want", fmt.Sprintf("%#o", want),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
//go:build windows
|
||||
|
||||
package permcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// check is the Windows-specific implementation of [Check].
|
||||
func check(ctx context.Context, l *slog.Logger, workDir, _, _, _, _ string) {
|
||||
dacl, owner, err := getSecurityInfo(workDir)
|
||||
if err != nil {
|
||||
l.ErrorContext(ctx, "getting security info", slogutil.KeyError, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !owner.IsWellKnown(windows.WinBuiltinAdministratorsSid) {
|
||||
l.WarnContext(ctx, "working directory owner is not in administrators group")
|
||||
}
|
||||
|
||||
err = rangeACEs(dacl, func(hdr windows.ACE_HEADER, mask windows.ACCESS_MASK, sid *windows.SID) (cont bool) {
|
||||
l.DebugContext(ctx, "checking entry", "sid", sid, "mask", mask)
|
||||
|
||||
warn := false
|
||||
switch {
|
||||
case hdr.AceType != windows.ACCESS_ALLOWED_ACE_TYPE:
|
||||
// Skip non-allowed ACEs.
|
||||
case !sid.IsWellKnown(windows.WinBuiltinAdministratorsSid):
|
||||
// Non-administrator ACEs should not have any access rights.
|
||||
warn = mask > 0
|
||||
default:
|
||||
// Administrators should full control access rights.
|
||||
warn = mask&fullControlMask != fullControlMask
|
||||
}
|
||||
if warn {
|
||||
l.WarnContext(ctx, "unexpected access control entry", "mask", mask, "sid", sid)
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
l.ErrorContext(ctx, "checking access control entries", slogutil.KeyError, err)
|
||||
}
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
package permcheck
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// NeedsMigration returns true if AdGuard Home files need permission migration.
|
||||
//
|
||||
// TODO(a.garipov): Consider ways to detect this better.
|
||||
func NeedsMigration(confFilePath string) (ok bool) {
|
||||
s, err := os.Stat(confFilePath)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// Likely a first run. Don't check.
|
||||
return false
|
||||
}
|
||||
|
||||
log.Error("permcheck: checking if files need migration: %s", err)
|
||||
|
||||
// Unexpected error. Try to migrate just in case.
|
||||
return true
|
||||
}
|
||||
|
||||
return s.Mode().Perm() != aghos.DefaultPermFile
|
||||
}
|
||||
|
||||
// Migrate attempts to change the permissions of AdGuard Home's files. It logs
|
||||
// the results at an appropriate level.
|
||||
func Migrate(workDir, dataDir, statsDir, querylogDir, confFilePath string) {
|
||||
chmodDir(workDir)
|
||||
|
||||
chmodFile(confFilePath)
|
||||
|
||||
// TODO(a.garipov): Put all paths in one place and remove this duplication.
|
||||
chmodDir(dataDir)
|
||||
chmodDir(filepath.Join(dataDir, "filters"))
|
||||
chmodFile(filepath.Join(dataDir, "sessions.db"))
|
||||
chmodFile(filepath.Join(dataDir, "leases.json"))
|
||||
|
||||
if dataDir != querylogDir {
|
||||
chmodDir(querylogDir)
|
||||
}
|
||||
chmodFile(filepath.Join(querylogDir, "querylog.json"))
|
||||
chmodFile(filepath.Join(querylogDir, "querylog.json.1"))
|
||||
|
||||
if dataDir != statsDir {
|
||||
chmodDir(statsDir)
|
||||
}
|
||||
chmodFile(filepath.Join(statsDir, "stats.db"))
|
||||
}
|
||||
|
||||
// chmodDir changes the permissions of a single directory. The results are
|
||||
// logged at the appropriate level.
|
||||
func chmodDir(dirPath string) {
|
||||
chmodPath(dirPath, typeDir, aghos.DefaultPermDir)
|
||||
}
|
||||
|
||||
// chmodFile changes the permissions of a single file. The results are logged
|
||||
// at the appropriate level.
|
||||
func chmodFile(filePath string) {
|
||||
chmodPath(filePath, typeFile, aghos.DefaultPermFile)
|
||||
}
|
||||
|
||||
// chmodPath changes the permissions of a single filesystem entity. The results
|
||||
// are logged at the appropriate level.
|
||||
func chmodPath(entPath, fileType string, fm fs.FileMode) {
|
||||
err := os.Chmod(entPath, fm)
|
||||
if err == nil {
|
||||
log.Info("permcheck: changed permissions for %s %q", fileType, entPath)
|
||||
|
||||
return
|
||||
} else if errors.Is(err, os.ErrNotExist) {
|
||||
log.Debug("permcheck: changing permissions for %s %q: %s", fileType, entPath, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Error(
|
||||
"permcheck: SECURITY WARNING: cannot change permissions for %s %q to %#o: %s; "+
|
||||
"this can leave your system vulnerable, see "+
|
||||
"https://adguard-dns.io/kb/adguard-home/running-securely/#os-service-concerns",
|
||||
fileType,
|
||||
entPath,
|
||||
fm,
|
||||
err,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
//go:build unix
|
||||
|
||||
package permcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
)
|
||||
|
||||
// needsMigration is a Unix-specific implementation of [NeedsMigration].
|
||||
//
|
||||
// TODO(a.garipov): Consider ways to detect this better.
|
||||
func needsMigration(ctx context.Context, l *slog.Logger, _, confFilePath string) (ok bool) {
|
||||
s, err := os.Stat(confFilePath)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// Likely a first run. Don't check.
|
||||
return false
|
||||
}
|
||||
|
||||
l.ErrorContext(ctx, "checking a need for permission migration", slogutil.KeyError, err)
|
||||
|
||||
// Unexpected error. Try to migrate just in case.
|
||||
return true
|
||||
}
|
||||
|
||||
return s.Mode().Perm() != aghos.DefaultPermFile
|
||||
}
|
||||
|
||||
// migrate is a Unix-specific implementation of [Migrate].
|
||||
func migrate(
|
||||
ctx context.Context,
|
||||
l *slog.Logger,
|
||||
workDir string,
|
||||
dataDir string,
|
||||
statsDir string,
|
||||
querylogDir string,
|
||||
confFilePath string,
|
||||
) {
|
||||
chmodDir(ctx, l, workDir)
|
||||
|
||||
chmodFile(ctx, l, confFilePath)
|
||||
|
||||
// TODO(a.garipov): Put all paths in one place and remove this duplication.
|
||||
chmodDir(ctx, l, dataDir)
|
||||
chmodDir(ctx, l, filepath.Join(dataDir, "filters"))
|
||||
chmodFile(ctx, l, filepath.Join(dataDir, "sessions.db"))
|
||||
chmodFile(ctx, l, filepath.Join(dataDir, "leases.json"))
|
||||
|
||||
if dataDir != querylogDir {
|
||||
chmodDir(ctx, l, querylogDir)
|
||||
}
|
||||
chmodFile(ctx, l, filepath.Join(querylogDir, "querylog.json"))
|
||||
chmodFile(ctx, l, filepath.Join(querylogDir, "querylog.json.1"))
|
||||
|
||||
if dataDir != statsDir {
|
||||
chmodDir(ctx, l, statsDir)
|
||||
}
|
||||
chmodFile(ctx, l, filepath.Join(statsDir, "stats.db"))
|
||||
}
|
||||
|
||||
// chmodDir changes the permissions of a single directory. The results are
|
||||
// logged at the appropriate level.
|
||||
func chmodDir(ctx context.Context, l *slog.Logger, dirPath string) {
|
||||
chmodPath(ctx, l, dirPath, typeDir, aghos.DefaultPermDir)
|
||||
}
|
||||
|
||||
// chmodFile changes the permissions of a single file. The results are logged
|
||||
// at the appropriate level.
|
||||
func chmodFile(ctx context.Context, l *slog.Logger, filePath string) {
|
||||
chmodPath(ctx, l, filePath, typeFile, aghos.DefaultPermFile)
|
||||
}
|
||||
|
||||
// chmodPath changes the permissions of a single filesystem entity. The results
|
||||
// are logged at the appropriate level.
|
||||
func chmodPath(ctx context.Context, l *slog.Logger, entPath, fileType string, fm fs.FileMode) {
|
||||
switch err := os.Chmod(entPath, fm); {
|
||||
case err == nil:
|
||||
l.InfoContext(ctx, "changed permissions", "type", fileType, "path", entPath)
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
l.DebugContext(
|
||||
ctx,
|
||||
"changing permissions",
|
||||
"type", fileType,
|
||||
"path", entPath,
|
||||
slogutil.KeyError, err,
|
||||
)
|
||||
default:
|
||||
l.ErrorContext(
|
||||
ctx,
|
||||
"can not change permissions; this can leave your system vulnerable, see "+
|
||||
"https://adguard-dns.io/kb/adguard-home/running-securely/#os-service-concerns",
|
||||
"type", fileType,
|
||||
"path", entPath,
|
||||
"target_perm", fmt.Sprintf("%#o", fm),
|
||||
slogutil.KeyError, err,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
//go:build windows
|
||||
|
||||
package permcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// needsMigration is the Windows-specific implementation of [NeedsMigration].
|
||||
func needsMigration(ctx context.Context, l *slog.Logger, workDir, _ string) (ok bool) {
|
||||
dacl, owner, err := getSecurityInfo(workDir)
|
||||
if err != nil {
|
||||
l.ErrorContext(ctx, "getting security info", slogutil.KeyError, err)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if !owner.IsWellKnown(windows.WinBuiltinAdministratorsSid) {
|
||||
return true
|
||||
}
|
||||
|
||||
err = rangeACEs(dacl, func(
|
||||
hdr windows.ACE_HEADER,
|
||||
mask windows.ACCESS_MASK,
|
||||
sid *windows.SID,
|
||||
) (cont bool) {
|
||||
switch {
|
||||
case hdr.AceType != windows.ACCESS_ALLOWED_ACE_TYPE:
|
||||
// Skip non-allowed access control entries.
|
||||
case !sid.IsWellKnown(windows.WinBuiltinAdministratorsSid):
|
||||
// Non-administrator access control entries should not have any
|
||||
// access rights.
|
||||
ok = mask > 0
|
||||
default:
|
||||
// Administrators should have full control.
|
||||
ok = mask&fullControlMask != fullControlMask
|
||||
}
|
||||
|
||||
// Stop ranging if the access control entry is unexpected.
|
||||
return !ok
|
||||
})
|
||||
if err != nil {
|
||||
l.ErrorContext(ctx, "checking access control entries", slogutil.KeyError, err)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
// migrate is the Windows-specific implementation of [Migrate].
|
||||
func migrate(ctx context.Context, l *slog.Logger, workDir, _, _, _, _ string) {
|
||||
dacl, owner, err := getSecurityInfo(workDir)
|
||||
if err != nil {
|
||||
l.ErrorContext(ctx, "getting security info", slogutil.KeyError, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !owner.IsWellKnown(windows.WinBuiltinAdministratorsSid) {
|
||||
var admins *windows.SID
|
||||
admins, err = windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
|
||||
if err != nil {
|
||||
l.ErrorContext(ctx, "creating administrators sid", slogutil.KeyError, err)
|
||||
} else {
|
||||
l.InfoContext(ctx, "migrating working directory owner", "sid", admins)
|
||||
owner = admins
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(e.burkov): Check for duplicates?
|
||||
var accessEntries []windows.EXPLICIT_ACCESS
|
||||
err = rangeACEs(dacl, func(
|
||||
hdr windows.ACE_HEADER,
|
||||
mask windows.ACCESS_MASK,
|
||||
sid *windows.SID,
|
||||
) (cont bool) {
|
||||
switch {
|
||||
case hdr.AceType != windows.ACCESS_ALLOWED_ACE_TYPE:
|
||||
// Add non-allowed access control entries as is.
|
||||
l.InfoContext(ctx, "migrating deny control entry", "sid", sid)
|
||||
accessEntries = append(accessEntries, newDenyExplicitAccess(sid, mask))
|
||||
case !sid.IsWellKnown(windows.WinBuiltinAdministratorsSid):
|
||||
// Skip non-administrator ACEs.
|
||||
l.InfoContext(ctx, "removing access control entry", "sid", sid)
|
||||
default:
|
||||
l.InfoContext(ctx, "migrating access control entry", "sid", sid, "mask", mask)
|
||||
accessEntries = append(accessEntries, newFullExplicitAccess(sid))
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
l.ErrorContext(ctx, "ranging trough access control entries", slogutil.KeyError, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = setSecurityInfo(workDir, owner, accessEntries)
|
||||
if err != nil {
|
||||
l.ErrorContext(ctx, "setting security info", slogutil.KeyError, err)
|
||||
}
|
||||
}
|
|
@ -1,20 +1,15 @@
|
|||
// Package permcheck contains code for simplifying permissions checks on files
|
||||
// and directories.
|
||||
//
|
||||
// TODO(a.garipov): Improve the approach on Windows.
|
||||
package permcheck
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"context"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// File type constants for logging.
|
||||
//
|
||||
// TODO(e.burkov): !! Use in Windows logging.
|
||||
const (
|
||||
typeDir = "directory"
|
||||
typeFile = "file"
|
||||
|
@ -22,65 +17,33 @@ const (
|
|||
|
||||
// Check checks the permissions on important files. It logs the results at
|
||||
// appropriate levels.
|
||||
func Check(workDir, dataDir, statsDir, querylogDir, confFilePath string) {
|
||||
checkDir(workDir)
|
||||
|
||||
checkFile(confFilePath)
|
||||
|
||||
// TODO(a.garipov): Put all paths in one place and remove this duplication.
|
||||
checkDir(dataDir)
|
||||
checkDir(filepath.Join(dataDir, "filters"))
|
||||
checkFile(filepath.Join(dataDir, "sessions.db"))
|
||||
checkFile(filepath.Join(dataDir, "leases.json"))
|
||||
|
||||
if dataDir != querylogDir {
|
||||
checkDir(querylogDir)
|
||||
}
|
||||
checkFile(filepath.Join(querylogDir, "querylog.json"))
|
||||
checkFile(filepath.Join(querylogDir, "querylog.json.1"))
|
||||
|
||||
if dataDir != statsDir {
|
||||
checkDir(statsDir)
|
||||
}
|
||||
checkFile(filepath.Join(statsDir, "stats.db"))
|
||||
func Check(
|
||||
ctx context.Context,
|
||||
l *slog.Logger,
|
||||
workDir string,
|
||||
dataDir string,
|
||||
statsDir string,
|
||||
querylogDir string,
|
||||
confFilePath string,
|
||||
) {
|
||||
check(ctx, l, workDir, dataDir, statsDir, querylogDir, confFilePath)
|
||||
}
|
||||
|
||||
// checkDir checks the permissions of a single directory. The results are
|
||||
// logged at the appropriate level.
|
||||
func checkDir(dirPath string) {
|
||||
checkPath(dirPath, typeDir, aghos.DefaultPermDir)
|
||||
// NeedsMigration returns true if AdGuard Home files need permission migration.
|
||||
func NeedsMigration(ctx context.Context, l *slog.Logger, workDir, confFilePath string) (ok bool) {
|
||||
return needsMigration(ctx, l, workDir, confFilePath)
|
||||
}
|
||||
|
||||
// checkFile checks the permissions of a single file. The results are logged at
|
||||
// the appropriate level.
|
||||
func checkFile(filePath string) {
|
||||
checkPath(filePath, typeFile, aghos.DefaultPermFile)
|
||||
}
|
||||
|
||||
// checkPath checks the permissions of a single filesystem entity. The results
|
||||
// are logged at the appropriate level.
|
||||
func checkPath(entPath, fileType string, want fs.FileMode) {
|
||||
s, err := os.Stat(entPath)
|
||||
if err != nil {
|
||||
logFunc := log.Error
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
logFunc = log.Debug
|
||||
}
|
||||
|
||||
logFunc("permcheck: checking %s %q: %s", fileType, entPath, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Add a more fine-grained check and result reporting.
|
||||
perm := s.Mode().Perm()
|
||||
if perm != want {
|
||||
log.Info(
|
||||
"permcheck: SECURITY WARNING: %s %q has unexpected permissions %#o; want %#o",
|
||||
fileType,
|
||||
entPath,
|
||||
perm,
|
||||
want,
|
||||
)
|
||||
}
|
||||
// Migrate attempts to change the permissions of AdGuard Home's files. It logs
|
||||
// the results at an appropriate level.
|
||||
func Migrate(
|
||||
ctx context.Context,
|
||||
l *slog.Logger,
|
||||
workDir string,
|
||||
dataDir string,
|
||||
statsDir string,
|
||||
querylogDir string,
|
||||
confFilePath string,
|
||||
) {
|
||||
migrate(ctx, l, workDir, dataDir, statsDir, querylogDir, confFilePath)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
//go:build windows
|
||||
|
||||
package permcheck
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unsafe"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// securityInfo defines the parts of a security descriptor to retrieve and set.
|
||||
const securityInfo windows.SECURITY_INFORMATION = windows.OWNER_SECURITY_INFORMATION |
|
||||
windows.DACL_SECURITY_INFORMATION |
|
||||
windows.PROTECTED_DACL_SECURITY_INFORMATION |
|
||||
windows.UNPROTECTED_DACL_SECURITY_INFORMATION
|
||||
|
||||
const objectType = windows.SE_FILE_OBJECT
|
||||
|
||||
// fileDeleteChildRight is the mask bit for the right to delete a child object.
|
||||
// It seems to be missing from the windows package.
|
||||
//
|
||||
// See https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/access-mask.
|
||||
const fileDeleteChildRight = 0b1000000
|
||||
|
||||
// fullControlMask is the mask for full control access rights.
|
||||
const fullControlMask windows.ACCESS_MASK = windows.FILE_LIST_DIRECTORY |
|
||||
windows.FILE_WRITE_DATA |
|
||||
windows.FILE_APPEND_DATA |
|
||||
windows.FILE_READ_EA |
|
||||
windows.FILE_WRITE_EA |
|
||||
windows.FILE_TRAVERSE |
|
||||
fileDeleteChildRight |
|
||||
windows.FILE_READ_ATTRIBUTES |
|
||||
windows.FILE_WRITE_ATTRIBUTES |
|
||||
windows.DELETE |
|
||||
windows.READ_CONTROL |
|
||||
windows.WRITE_DAC |
|
||||
windows.WRITE_OWNER |
|
||||
windows.SYNCHRONIZE
|
||||
|
||||
// aceFunc is a function that handles access control entries in the
|
||||
// discretionary access control list. It should return true to continue ranging
|
||||
// or false to stop.
|
||||
type aceFunc = func(
|
||||
hdr windows.ACE_HEADER,
|
||||
mask windows.ACCESS_MASK,
|
||||
sid *windows.SID,
|
||||
) (cont bool)
|
||||
|
||||
// rangeACEs ranges over the access control entries in the discretionary access
|
||||
// control list of the specified security descriptor.
|
||||
func rangeACEs(dacl *windows.ACL, f aceFunc) (err error) {
|
||||
var errs []error
|
||||
for i := range uint32(dacl.AceCount) {
|
||||
var ace *windows.ACCESS_ALLOWED_ACE
|
||||
err = windows.GetAce(dacl, i, &ace)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("getting entry at index %d: %w", i, err))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
sid := (*windows.SID)(unsafe.Pointer(&ace.SidStart))
|
||||
if !f(ace.Header, ace.Mask, sid) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err = errors.Join(errs...); err != nil {
|
||||
return fmt.Errorf("checking access control entries: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setSecurityInfo sets the security information on the specified file, using
|
||||
// ents to create a discretionary access control list.
|
||||
func setSecurityInfo(fname string, owner *windows.SID, ents []windows.EXPLICIT_ACCESS) (err error) {
|
||||
if len(ents) == 0 {
|
||||
ents = []windows.EXPLICIT_ACCESS{
|
||||
newFullExplicitAccess(owner),
|
||||
}
|
||||
}
|
||||
|
||||
acl, err := windows.ACLFromEntries(ents, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating access control list: %w", err)
|
||||
}
|
||||
|
||||
err = windows.SetNamedSecurityInfo(fname, objectType, securityInfo, owner, nil, acl, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting security info: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSecurityInfo retrieves the security information for the specified file.
|
||||
func getSecurityInfo(fname string) (dacl *windows.ACL, owner *windows.SID, err error) {
|
||||
sd, err := windows.GetNamedSecurityInfo(fname, objectType, securityInfo)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("getting security descriptor: %w", err)
|
||||
}
|
||||
|
||||
owner, _, err = sd.Owner()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("getting owner sid: %w", err)
|
||||
}
|
||||
|
||||
dacl, _, err = sd.DACL()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("getting discretionary access control list: %w", err)
|
||||
}
|
||||
|
||||
return dacl, owner, nil
|
||||
}
|
||||
|
||||
// newFullExplicitAccess creates a new explicit access entry with full control
|
||||
// permissions.
|
||||
func newFullExplicitAccess(sid *windows.SID) (expAcc windows.EXPLICIT_ACCESS) {
|
||||
// TODO(e.burkov): !! lookup account type
|
||||
return windows.EXPLICIT_ACCESS{
|
||||
AccessPermissions: fullControlMask,
|
||||
AccessMode: windows.GRANT_ACCESS,
|
||||
Inheritance: windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
|
||||
Trustee: windows.TRUSTEE{
|
||||
TrusteeForm: windows.TRUSTEE_IS_SID,
|
||||
TrusteeType: windows.TRUSTEE_IS_GROUP,
|
||||
TrusteeValue: windows.TrusteeValueFromSID(sid),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// newDenyExplicitAccess creates a new explicit access entry with specified deny
|
||||
// permissions.
|
||||
func newDenyExplicitAccess(sid *windows.SID, mask windows.ACCESS_MASK) (expAcc windows.EXPLICIT_ACCESS) {
|
||||
// TODO(e.burkov): !! lookup account type
|
||||
return windows.EXPLICIT_ACCESS{
|
||||
AccessPermissions: mask,
|
||||
AccessMode: windows.DENY_ACCESS,
|
||||
Inheritance: windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
|
||||
Trustee: windows.TRUSTEE{
|
||||
TrusteeForm: windows.TRUSTEE_IS_SID,
|
||||
TrusteeType: windows.TRUSTEE_IS_GROUP,
|
||||
TrusteeValue: windows.TrusteeValueFromSID(sid),
|
||||
},
|
||||
}
|
||||
}
|
|
@ -62,8 +62,8 @@ set -f -u
|
|||
# NOTE: Flag -H for grep is non-POSIX but all of Busybox, GNU, macOS, and
|
||||
# OpenBSD support it.
|
||||
#
|
||||
# NOTE: Exclude the permission_windows.go, because it requires unsafe for the
|
||||
# OS APIs.
|
||||
# NOTE: Exclude the security_windows.go, because it requires unsafe for the OS
|
||||
# APIs.
|
||||
#
|
||||
# TODO(a.garipov): Add golibs/log.
|
||||
blocklist_imports() {
|
||||
|
@ -72,7 +72,7 @@ blocklist_imports() {
|
|||
-name '*.go' \
|
||||
'!' '(' \
|
||||
-name '*.pb.go' \
|
||||
-o -path './internal/aghos/permission_windows.go' \
|
||||
-o -path './internal/permcheck/security_windows.go' \
|
||||
')' \
|
||||
-exec \
|
||||
'grep' \
|
||||
|
|
Loading…
Reference in New Issue