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 {
|
if !opts.noPermCheck {
|
||||||
checkPermissions(Context.workDir, confPath, dataDir, statsDir, querylogDir)
|
checkPermissions(ctx, slogLogger, Context.workDir, confPath, dataDir, statsDir, querylogDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.web.start()
|
Context.web.start()
|
||||||
|
@ -751,12 +751,22 @@ func newUpdater(
|
||||||
|
|
||||||
// checkPermissions checks and migrates permissions of the files and directories
|
// checkPermissions checks and migrates permissions of the files and directories
|
||||||
// used by AdGuard Home, if needed.
|
// used by AdGuard Home, if needed.
|
||||||
func checkPermissions(workDir, confPath, dataDir, statsDir, querylogDir string) {
|
func checkPermissions(
|
||||||
if permcheck.NeedsMigration(confPath) {
|
ctx context.Context,
|
||||||
permcheck.Migrate(workDir, dataDir, statsDir, querylogDir, confPath)
|
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.
|
// 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
|
// Package permcheck contains code for simplifying permissions checks on files
|
||||||
// and directories.
|
// and directories.
|
||||||
//
|
|
||||||
// TODO(a.garipov): Improve the approach on Windows.
|
|
||||||
package permcheck
|
package permcheck
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/fs"
|
"context"
|
||||||
"os"
|
"log/slog"
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
|
||||||
"github.com/AdguardTeam/golibs/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// File type constants for logging.
|
// File type constants for logging.
|
||||||
|
//
|
||||||
|
// TODO(e.burkov): !! Use in Windows logging.
|
||||||
const (
|
const (
|
||||||
typeDir = "directory"
|
typeDir = "directory"
|
||||||
typeFile = "file"
|
typeFile = "file"
|
||||||
|
@ -22,65 +17,33 @@ const (
|
||||||
|
|
||||||
// Check checks the permissions on important files. It logs the results at
|
// Check checks the permissions on important files. It logs the results at
|
||||||
// appropriate levels.
|
// appropriate levels.
|
||||||
func Check(workDir, dataDir, statsDir, querylogDir, confFilePath string) {
|
func Check(
|
||||||
checkDir(workDir)
|
ctx context.Context,
|
||||||
|
l *slog.Logger,
|
||||||
checkFile(confFilePath)
|
workDir string,
|
||||||
|
dataDir string,
|
||||||
// TODO(a.garipov): Put all paths in one place and remove this duplication.
|
statsDir string,
|
||||||
checkDir(dataDir)
|
querylogDir string,
|
||||||
checkDir(filepath.Join(dataDir, "filters"))
|
confFilePath string,
|
||||||
checkFile(filepath.Join(dataDir, "sessions.db"))
|
) {
|
||||||
checkFile(filepath.Join(dataDir, "leases.json"))
|
check(ctx, l, workDir, dataDir, statsDir, querylogDir, confFilePath)
|
||||||
|
|
||||||
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"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkDir checks the permissions of a single directory. The results are
|
// NeedsMigration returns true if AdGuard Home files need permission migration.
|
||||||
// logged at the appropriate level.
|
func NeedsMigration(ctx context.Context, l *slog.Logger, workDir, confFilePath string) (ok bool) {
|
||||||
func checkDir(dirPath string) {
|
return needsMigration(ctx, l, workDir, confFilePath)
|
||||||
checkPath(dirPath, typeDir, aghos.DefaultPermDir)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkFile checks the permissions of a single file. The results are logged at
|
// Migrate attempts to change the permissions of AdGuard Home's files. It logs
|
||||||
// the appropriate level.
|
// the results at an appropriate level.
|
||||||
func checkFile(filePath string) {
|
func Migrate(
|
||||||
checkPath(filePath, typeFile, aghos.DefaultPermFile)
|
ctx context.Context,
|
||||||
}
|
l *slog.Logger,
|
||||||
|
workDir string,
|
||||||
// checkPath checks the permissions of a single filesystem entity. The results
|
dataDir string,
|
||||||
// are logged at the appropriate level.
|
statsDir string,
|
||||||
func checkPath(entPath, fileType string, want fs.FileMode) {
|
querylogDir string,
|
||||||
s, err := os.Stat(entPath)
|
confFilePath string,
|
||||||
if err != nil {
|
) {
|
||||||
logFunc := log.Error
|
migrate(ctx, l, workDir, dataDir, statsDir, querylogDir, confFilePath)
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
# NOTE: Flag -H for grep is non-POSIX but all of Busybox, GNU, macOS, and
|
||||||
# OpenBSD support it.
|
# OpenBSD support it.
|
||||||
#
|
#
|
||||||
# NOTE: Exclude the permission_windows.go, because it requires unsafe for the
|
# NOTE: Exclude the security_windows.go, because it requires unsafe for the OS
|
||||||
# OS APIs.
|
# APIs.
|
||||||
#
|
#
|
||||||
# TODO(a.garipov): Add golibs/log.
|
# TODO(a.garipov): Add golibs/log.
|
||||||
blocklist_imports() {
|
blocklist_imports() {
|
||||||
|
@ -72,7 +72,7 @@ blocklist_imports() {
|
||||||
-name '*.go' \
|
-name '*.go' \
|
||||||
'!' '(' \
|
'!' '(' \
|
||||||
-name '*.pb.go' \
|
-name '*.pb.go' \
|
||||||
-o -path './internal/aghos/permission_windows.go' \
|
-o -path './internal/permcheck/security_windows.go' \
|
||||||
')' \
|
')' \
|
||||||
-exec \
|
-exec \
|
||||||
'grep' \
|
'grep' \
|
||||||
|
|
Loading…
Reference in New Issue