убрана лишняя зависимость bindfs и избыточное использование дополнительного пользователя alr

This commit is contained in:
2025-08-26 22:09:28 +03:00
parent ab41700004
commit 51fa7ca6fb
25 changed files with 274 additions and 405 deletions

View File

@@ -47,7 +47,7 @@ jobs:
- name: Prepare for install - name: Prepare for install
run: | run: |
apt-get update && apt-get install -y libcap2-bin bindfs apt-get update
- name: Build alr - name: Build alr
env: env:

7
.gitignore vendored
View File

@@ -3,11 +3,12 @@
/cmd/alr-api-server/alr-api-server /cmd/alr-api-server/alr-api-server
/dist/ /dist/
/internal/config/version.txt /internal/config/version.txt
.fleet .fleet/
.idea .idea/
.gigaide .gigaide/
*.out *.out
e2e-tests/alr e2e-tests/alr
CLAUDE.md
commit_msg.txt commit_msg.txt

View File

@@ -49,17 +49,12 @@ install: \
$(INSTALLED_BIN): $(BIN) $(INSTALLED_BIN): $(BIN)
install -Dm755 $< $@ install -Dm755 $< $@
ifeq ($(CREATE_SYSTEM_RESOURCES),1) ifeq ($(CREATE_SYSTEM_RESOURCES),1)
setcap cap_setuid,cap_setgid+ep $(INSTALLED_BIN)
@if id alr >/dev/null 2>&1; then \
echo "User 'alr' already exists. Skipping."; \
else \
useradd -r -s /usr/sbin/nologin alr; \
fi
@for dir in $(ROOT_DIRS); do \ @for dir in $(ROOT_DIRS); do \
install -d -o alr -g alr -m 755 $$dir; \ install -d -m 775 $$dir; \
chgrp wheel $$dir; \
done done
else else
@echo "Skipping user and root dir creation (CREATE_SYSTEM_RESOURCES=0)" @echo "Skipping root dir creation (CREATE_SYSTEM_RESOURCES=0)"
endif endif
$(INSTALLED_BASH_COMPLETION): $(BASH_COMPLETION) $(INSTALLED_BASH_COMPLETION): $(BASH_COMPLETION)

View File

@@ -72,12 +72,6 @@ func BuildCmd() *cli.Command {
return cliutils.FormatCliExit(gotext.Get("Error getting working directory"), err) return cliutils.FormatCliExit(gotext.Get("Error getting working directory"), err)
} }
wd, wdCleanup, err := Mount(wd)
if err != nil {
return err
}
defer wdCleanup()
ctx := c.Context ctx := c.Context
deps, err := appbuilder. deps, err := appbuilder.
@@ -156,19 +150,9 @@ func BuildCmd() *cli.Command {
return cliutils.FormatCliExit(gotext.Get("Nothing to build"), nil) return cliutils.FormatCliExit(gotext.Get("Nothing to build"), nil)
} }
if scriptArgs != nil {
scriptFile := filepath.Base(scriptArgs.Script)
newScriptDir, scriptDirCleanup, err := Mount(filepath.Dir(scriptArgs.Script))
if err != nil {
return err
}
defer scriptDirCleanup()
scriptArgs.Script = filepath.Join(newScriptDir, scriptFile)
}
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
return err
}
installer, installerClose, err := build.GetSafeInstaller() installer, installerClose, err := build.GetSafeInstaller()
if err != nil { if err != nil {
@@ -176,9 +160,7 @@ func BuildCmd() *cli.Command {
} }
defer installerClose() defer installerClose()
if err := utils.ExitIfCantSetNoNewPrivs(); err != nil {
return err
}
scripter, scripterClose, err := build.GetSafeScriptExecutor() scripter, scripterClose, err := build.GetSafeScriptExecutor()
if err != nil { if err != nil {

131
fix.go
View File

@@ -23,6 +23,7 @@ import (
"io/fs" "io/fs"
"log/slog" "log/slog"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"github.com/leonelquinteros/gotext" "github.com/leonelquinteros/gotext"
@@ -38,9 +39,8 @@ func FixCmd() *cli.Command {
Name: "fix", Name: "fix",
Usage: gotext.Get("Attempt to fix problems with ALR"), Usage: gotext.Get("Attempt to fix problems with ALR"),
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil { // Команда выполняется от текущего пользователя
return err // При необходимости будет запрошен sudo для удаления файлов root
}
ctx := c.Context ctx := c.Context
@@ -57,37 +57,126 @@ func FixCmd() *cli.Command {
paths := cfg.GetPaths() paths := cfg.GetPaths()
slog.Info(gotext.Get("Clearing cache directory")) slog.Info(gotext.Get("Clearing cache and temporary directories"))
// Проверяем, существует ли директория кэша
dir, err := os.Open(paths.CacheDir) dir, err := os.Open(paths.CacheDir)
if err != nil { if err != nil {
return cliutils.FormatCliExit(gotext.Get("Unable to open cache directory"), err) if os.IsNotExist(err) {
} // Директория не существует, просто создадим её позже
defer dir.Close() slog.Info(gotext.Get("Cache directory does not exist, will create it"))
} else {
return cliutils.FormatCliExit(gotext.Get("Unable to open cache directory"), err)
}
} else {
defer dir.Close()
entries, err := dir.Readdirnames(-1) entries, err := dir.Readdirnames(-1)
if err != nil { if err != nil {
return cliutils.FormatCliExit(gotext.Get("Unable to read cache directory contents"), err) return cliutils.FormatCliExit(gotext.Get("Unable to read cache directory contents"), err)
}
for _, entry := range entries {
fullPath := filepath.Join(paths.CacheDir, entry)
if err := makeWritableRecursive(fullPath); err != nil {
slog.Debug("Failed to make path writable", "path", fullPath, "error", err)
} }
err = os.RemoveAll(fullPath) for _, entry := range entries {
fullPath := filepath.Join(paths.CacheDir, entry)
// Пробуем сделать файлы доступными для записи
if err := makeWritableRecursive(fullPath); err != nil {
slog.Debug("Failed to make path writable", "path", fullPath, "error", err)
}
// Пробуем удалить
err = os.RemoveAll(fullPath)
if err != nil {
// Если не получилось удалить, пробуем через sudo
slog.Warn(gotext.Get("Unable to remove cache item (%s) as current user, trying with sudo", entry))
sudoCmd := exec.Command("sudo", "rm", "-rf", fullPath)
if sudoErr := sudoCmd.Run(); sudoErr != nil {
// Если и через sudo не получилось, пропускаем с предупреждением
slog.Error(gotext.Get("Unable to remove cache item (%s)", entry), "error", err)
continue
}
}
}
}
// Очищаем временные директории
slog.Info(gotext.Get("Clearing temporary directory"))
tmpDir := "/tmp/alr"
if _, err := os.Stat(tmpDir); err == nil {
// Директория существует, пробуем очистить
err = os.RemoveAll(tmpDir)
if err != nil { if err != nil {
return cliutils.FormatCliExit(gotext.Get("Unable to remove cache item (%s)", entry), err) // Если не получилось удалить, пробуем через sudo
slog.Warn(gotext.Get("Unable to remove temporary directory as current user, trying with sudo"))
sudoCmd := exec.Command("sudo", "rm", "-rf", tmpDir)
if sudoErr := sudoCmd.Run(); sudoErr != nil {
slog.Error(gotext.Get("Unable to remove temporary directory"), "error", err)
}
}
}
// Создаем базовый каталог /tmp/alr с владельцем root:wheel и правами 775
err = utils.EnsureTempDirWithRootOwner(tmpDir, 0o775)
if err != nil {
slog.Warn(gotext.Get("Unable to create temporary directory"), "error", err)
}
// Создаем каталог dl с правами для группы wheel
dlDir := filepath.Join(tmpDir, "dl")
err = utils.EnsureTempDirWithRootOwner(dlDir, 0o775)
if err != nil {
slog.Warn(gotext.Get("Unable to create download directory"), "error", err)
}
// Создаем каталог pkgs с правами для группы wheel
pkgsDir := filepath.Join(tmpDir, "pkgs")
err = utils.EnsureTempDirWithRootOwner(pkgsDir, 0o775)
if err != nil {
slog.Warn(gotext.Get("Unable to create packages directory"), "error", err)
}
// Исправляем права на все существующие файлы в /tmp/alr, если там что-то есть
if _, err := os.Stat(tmpDir); err == nil {
slog.Info(gotext.Get("Fixing permissions on temporary files"))
// Проверяем, есть ли файлы в директории
entries, err := os.ReadDir(tmpDir)
if err == nil && len(entries) > 0 {
fixCmd := exec.Command("sudo", "chown", "-R", "root:wheel", tmpDir)
if fixErr := fixCmd.Run(); fixErr != nil {
slog.Warn(gotext.Get("Unable to fix file ownership"), "error", fixErr)
}
fixCmd = exec.Command("sudo", "chmod", "-R", "2775", tmpDir)
if fixErr := fixCmd.Run(); fixErr != nil {
slog.Warn(gotext.Get("Unable to fix file permissions"), "error", fixErr)
}
} }
} }
slog.Info(gotext.Get("Rebuilding cache")) slog.Info(gotext.Get("Rebuilding cache"))
err = os.MkdirAll(paths.CacheDir, 0o755) // Пробуем создать директорию кэша
err = os.MkdirAll(paths.CacheDir, 0o775)
if err != nil { if err != nil {
return cliutils.FormatCliExit(gotext.Get("Unable to create new cache directory"), err) // Если не получилось, пробуем через sudo с правильными правами для группы wheel
slog.Info(gotext.Get("Creating cache directory with sudo"))
sudoCmd := exec.Command("sudo", "mkdir", "-p", paths.CacheDir)
if sudoErr := sudoCmd.Run(); sudoErr != nil {
return cliutils.FormatCliExit(gotext.Get("Unable to create new cache directory"), err)
}
// Устанавливаем права 775 и группу wheel
chmodCmd := exec.Command("sudo", "chmod", "775", paths.CacheDir)
if chmodErr := chmodCmd.Run(); chmodErr != nil {
return cliutils.FormatCliExit(gotext.Get("Unable to set cache directory permissions"), chmodErr)
}
chgrpCmd := exec.Command("sudo", "chgrp", "wheel", paths.CacheDir)
if chgrpErr := chgrpCmd.Run(); chgrpErr != nil {
return cliutils.FormatCliExit(gotext.Get("Unable to set cache directory group"), chgrpErr)
}
} }
deps, err = appbuilder. deps, err = appbuilder.

View File

@@ -31,7 +31,6 @@ import (
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils" "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder" appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides" "gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
) )
@@ -48,9 +47,6 @@ func InfoCmd() *cli.Command {
}, },
}, },
BashComplete: cliutils.BashCompleteWithError(func(c *cli.Context) error { BashComplete: cliutils.BashCompleteWithError(func(c *cli.Context) error {
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
return err
}
ctx := c.Context ctx := c.Context
deps, err := appbuilder. deps, err := appbuilder.
@@ -74,9 +70,7 @@ func InfoCmd() *cli.Command {
return nil return nil
}), }),
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil { // Запуск от текущего пользователя
return err
}
args := c.Args() args := c.Args()
if args.Len() < 1 { if args.Len() < 1 {

View File

@@ -51,9 +51,6 @@ func InstallCmd() *cli.Command {
return cliutils.FormatCliExit(gotext.Get("Command install expected at least 1 argument, got %d", args.Len()), nil) return cliutils.FormatCliExit(gotext.Get("Command install expected at least 1 argument, got %d", args.Len()), nil)
} }
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
return err
}
installer, installerClose, err := build.GetSafeInstaller() installer, installerClose, err := build.GetSafeInstaller()
if err != nil { if err != nil {
@@ -61,9 +58,6 @@ func InstallCmd() *cli.Command {
} }
defer installerClose() defer installerClose()
if err := utils.ExitIfCantSetNoNewPrivs(); err != nil {
return err
}
scripter, scripterClose, err := build.GetSafeScriptExecutor() scripter, scripterClose, err := build.GetSafeScriptExecutor()
if err != nil { if err != nil {
@@ -116,9 +110,6 @@ func InstallCmd() *cli.Command {
return nil return nil
}), }),
BashComplete: cliutils.BashCompleteWithError(func(c *cli.Context) error { BashComplete: cliutils.BashCompleteWithError(func(c *cli.Context) error {
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
return err
}
ctx := c.Context ctx := c.Context
deps, err := appbuilder. deps, err := appbuilder.

View File

@@ -17,14 +17,8 @@
package main package main
import ( import (
"bufio"
"errors"
"fmt"
"log/slog" "log/slog"
"os" "os"
"os/exec"
"os/user"
"path/filepath"
"syscall" "syscall"
"github.com/hashicorp/go-hclog" "github.com/hashicorp/go-hclog"
@@ -36,7 +30,6 @@ import (
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils" "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder" appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/config" "gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/constants"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/logger" "gitea.plemya-x.ru/Plemya-x/ALR/internal/logger"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager" "gitea.plemya-x.ru/Plemya-x/ALR/internal/manager"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils" "gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
@@ -52,9 +45,6 @@ func InternalBuildCmd() *cli.Command {
slog.Debug("start _internal-safe-script-executor", "uid", syscall.Getuid(), "gid", syscall.Getgid()) slog.Debug("start _internal-safe-script-executor", "uid", syscall.Getuid(), "gid", syscall.Getgid())
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
return err
}
cfg := config.New() cfg := config.New()
err := cfg.Load() err := cfg.Load()
@@ -92,9 +82,6 @@ func InternalReposCmd() *cli.Command {
Action: utils.RootNeededAction(func(ctx *cli.Context) error { Action: utils.RootNeededAction(func(ctx *cli.Context) error {
logger.SetupForGoPlugin() logger.SetupForGoPlugin()
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
return err
}
deps, err := appbuilder. deps, err := appbuilder.
New(ctx.Context). New(ctx.Context).
@@ -129,16 +116,7 @@ func InternalInstallCmd() *cli.Command {
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
logger.SetupForGoPlugin() logger.SetupForGoPlugin()
if err := utils.EnsureIsAlrUser(); err != nil { // Запуск от текущего пользователя, повышение прав будет через sudo при необходимости
return err
}
// Before escalating the rights, we made sure that
// this is an ALR user, so it looks safe.
err := utils.EscalateToRootUid()
if err != nil {
return cliutils.FormatCliExit("cannot escalate to root", err)
}
deps, err := appbuilder. deps, err := appbuilder.
New(c.Context). New(c.Context).
@@ -175,143 +153,4 @@ func InternalInstallCmd() *cli.Command {
} }
} }
func Mount(target string) (string, func(), error) {
exe, err := os.Executable()
if err != nil {
return "", nil, fmt.Errorf("failed to get executable path: %w", err)
}
cmd := exec.Command(exe, "_internal-temporary-mount", target)
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return "", nil, fmt.Errorf("failed to get stdout pipe: %w", err)
}
stdinPipe, err := cmd.StdinPipe()
if err != nil {
return "", nil, fmt.Errorf("failed to get stdin pipe: %w", err)
}
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
return "", nil, fmt.Errorf("failed to start mount: %w", err)
}
scanner := bufio.NewScanner(stdoutPipe)
var mountPath string
if scanner.Scan() {
mountPath = scanner.Text()
}
if err := scanner.Err(); err != nil {
_ = cmd.Process.Kill()
return "", nil, fmt.Errorf("failed to read mount output: %w", err)
}
if mountPath == "" {
_ = cmd.Process.Kill()
return "", nil, errors.New("mount failed: no target path returned")
}
cleanup := func() {
slog.Debug("cleanup triggered")
_, _ = fmt.Fprintln(stdinPipe, "")
_ = cmd.Wait()
}
return mountPath, cleanup, nil
}
func InternalMountCmd() *cli.Command {
return &cli.Command{
Name: "_internal-temporary-mount",
HideHelp: true,
Hidden: true,
Action: func(c *cli.Context) error {
logger.SetupForGoPlugin()
sourceDir := c.Args().First()
u, err := user.Current()
if err != nil {
return cliutils.FormatCliExit("cannot get current user", err)
}
_, alrGid, err := utils.GetUidGidAlrUser()
if err != nil {
return cliutils.FormatCliExit("cannot get alr user", err)
}
if _, err := os.Stat(sourceDir); err != nil {
return cliutils.FormatCliExit(fmt.Sprintf("cannot read %s", sourceDir), err)
}
if err := utils.EnuseIsPrivilegedGroupMember(); err != nil {
return err
}
// Before escalating the rights, we made sure that
// 1. user in wheel group
// 2. user can access sourceDir
if err := utils.EscalateToRootUid(); err != nil {
return err
}
if err := syscall.Setgid(alrGid); err != nil {
return err
}
if err := os.MkdirAll(constants.AlrRunDir, 0o770); err != nil {
return cliutils.FormatCliExit(fmt.Sprintf("failed to create %s", constants.AlrRunDir), err)
}
if err := os.Chown(constants.AlrRunDir, 0, alrGid); err != nil {
return cliutils.FormatCliExit(fmt.Sprintf("failed to chown %s", constants.AlrRunDir), err)
}
targetDir := filepath.Join(constants.AlrRunDir, fmt.Sprintf("bindfs-%d", os.Getpid()))
// 0750: owner (root) and group (alr)
if err := os.MkdirAll(targetDir, 0o750); err != nil {
return cliutils.FormatCliExit("error creating bindfs target directory", err)
}
// chown AlrRunDir/mounts/bindfs-* to (root:alr),
// so alr user can access dir
if err := os.Chown(targetDir, 0, alrGid); err != nil {
return cliutils.FormatCliExit("failed to chown bindfs directory", err)
}
bindfsCmd := exec.Command(
"bindfs",
fmt.Sprintf("--map=%s/alr:@%s/@alr", u.Uid, u.Gid),
sourceDir,
targetDir,
)
bindfsCmd.Stderr = os.Stderr
if err := bindfsCmd.Run(); err != nil {
return cliutils.FormatCliExit("failed to strart bindfs", err)
}
fmt.Println(targetDir)
_, _ = bufio.NewReader(os.Stdin).ReadString('\n')
slog.Debug("start unmount", "dir", targetDir)
umountCmd := exec.Command("umount", targetDir)
umountCmd.Stderr = os.Stderr
if err := umountCmd.Run(); err != nil {
return cliutils.FormatCliExit(fmt.Sprintf("failed to unmount %s", targetDir), err)
}
if err := os.Remove(targetDir); err != nil {
return cliutils.FormatCliExit(fmt.Sprintf("error removing directory %s", targetDir), err)
}
return nil
},
}
}

View File

@@ -44,9 +44,9 @@ var HandshakeConfig = plugin.HandshakeConfig{
func setCommonCmdEnv(cmd *exec.Cmd) { func setCommonCmdEnv(cmd *exec.Cmd) {
cmd.Env = []string{ cmd.Env = []string{
"HOME=/var/cache/alr", "HOME=" + os.Getenv("HOME"),
"LOGNAME=alr", "LOGNAME=" + os.Getenv("USER"),
"USER=alr", "USER=" + os.Getenv("USER"),
"PATH=/usr/bin:/bin:/usr/local/bin", "PATH=/usr/bin:/bin:/usr/local/bin",
} }
for _, env := range os.Environ() { for _, env := range os.Environ() {
@@ -102,9 +102,7 @@ func getSafeExecutor[T any](subCommand, pluginName string) (T, func(), error) {
Cmd: cmd, Cmd: cmd,
Logger: logger.GetHCLoggerAdapter(), Logger: logger.GetHCLoggerAdapter(),
SkipHostEnv: true, SkipHostEnv: true,
UnixSocketConfig: &plugin.UnixSocketConfig{ UnixSocketConfig: &plugin.UnixSocketConfig{},
Group: "alr",
},
SyncStderr: os.Stderr, SyncStderr: os.Stderr,
}) })
rpcClient, err := client.Client() rpcClient, err := client.Client()

View File

@@ -23,6 +23,7 @@ import (
"os" "os"
"strings" "strings"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/constants"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/dl" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/dl"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/dlcache" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/dlcache"
) )
@@ -74,7 +75,9 @@ func (s *SourceDownloader) DownloadSources(
} }
} }
opts.DlCache = dlcache.New(s.cfg.GetPaths().CacheDir) // Используем временную директорию для загрузок
// dlcache.New добавит свой подкаталог "dl" внутри
opts.DlCache = dlcache.New(constants.TempDir)
err := dl.Download(ctx, opts) err := dl.Download(ctx, opts)
if err != nil { if err != nil {

View File

@@ -40,6 +40,7 @@ import (
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cpu" "gitea.plemya-x.ru/Plemya-x/ALR/internal/cpu"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager" "gitea.plemya-x.ru/Plemya-x/ALR/internal/manager"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides" "gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
@@ -47,15 +48,21 @@ import (
// Функция prepareDirs подготавливает директории для сборки. // Функция prepareDirs подготавливает директории для сборки.
func prepareDirs(dirs types.Directories) error { func prepareDirs(dirs types.Directories) error {
err := os.RemoveAll(dirs.BaseDir) // Удаляем базовую директорию, если она существует // Пробуем удалить базовую директорию, если она существует
err := os.RemoveAll(dirs.BaseDir)
if err != nil {
// Если не можем удалить (например, принадлежит root), игнорируем
// и попробуем создать новые директории
}
// Создаем директории с правильным владельцем для /tmp/alr с setgid битом
err = utils.EnsureTempDirWithRootOwner(dirs.SrcDir, 0o2775)
if err != nil { if err != nil {
return err return err
} }
err = os.MkdirAll(dirs.SrcDir, 0o755) // Создаем директорию для источников
if err != nil { // Создаем директорию для пакетов с setgid битом
return err return utils.EnsureTempDirWithRootOwner(dirs.PkgDir, 0o2775)
}
return os.MkdirAll(dirs.PkgDir, 0o755) // Создаем директорию для пакетов
} }
// Функция buildContents создает секцию содержимого пакета, которая содержит файлы, // Функция buildContents создает секцию содержимого пакета, которая содержит файлы,

View File

@@ -21,6 +21,7 @@ package config
import ( import (
"fmt" "fmt"
"os"
"path/filepath" "path/filepath"
"github.com/goccy/go-yaml" "github.com/goccy/go-yaml"
@@ -55,7 +56,12 @@ func defaultConfigKoanf() *koanf.Koanf {
"ignorePkgUpdates": []string{}, "ignorePkgUpdates": []string{},
"logLevel": "info", "logLevel": "info",
"autoPull": true, "autoPull": true,
"repos": []types.Repo{}, "repos": []types.Repo{
{
Name: "alr-default",
URL: "https://gitea.plemya-x.ru/Plemya-x/alr-default.git",
},
},
} }
if err := k.Load(confmap.Provider(defaults, "."), nil); err != nil { if err := k.Load(confmap.Provider(defaults, "."), nil); err != nil {
panic(k) panic(k)
@@ -98,8 +104,15 @@ func (c *ALRConfig) Load() error {
c.paths.UserConfigPath = constants.SystemConfigPath c.paths.UserConfigPath = constants.SystemConfigPath
c.paths.CacheDir = constants.SystemCachePath c.paths.CacheDir = constants.SystemCachePath
c.paths.RepoDir = filepath.Join(c.paths.CacheDir, "repo") c.paths.RepoDir = filepath.Join(c.paths.CacheDir, "repo")
c.paths.PkgsDir = filepath.Join(c.paths.CacheDir, "pkgs") c.paths.PkgsDir = filepath.Join(constants.TempDir, "pkgs") // Перемещаем в /tmp/alr/pkgs
c.paths.DBPath = filepath.Join(c.paths.CacheDir, "db") c.paths.DBPath = filepath.Join(c.paths.CacheDir, "alr.db")
// Проверяем существование кэш-директории, но не пытаемся создать
if _, err := os.Stat(c.paths.CacheDir); err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("failed to check cache directory: %w", err)
}
}
return nil return nil
} }

View File

@@ -19,6 +19,6 @@ package constants
const ( const (
SystemConfigPath = "/etc/alr/alr.toml" SystemConfigPath = "/etc/alr/alr.toml"
SystemCachePath = "/var/cache/alr" SystemCachePath = "/var/cache/alr"
AlrRunDir = "/var/run/alr" TempDir = "/tmp/alr"
PrivilegedGroup = "wheel" PrivilegedGroup = "wheel"
) )

View File

@@ -21,7 +21,10 @@ package db
import ( import (
"context" "context"
"fmt"
"log/slog" "log/slog"
"os"
"path/filepath"
"github.com/leonelquinteros/gotext" "github.com/leonelquinteros/gotext"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
@@ -54,6 +57,21 @@ func New(config Config) *Database {
func (d *Database) Connect() error { func (d *Database) Connect() error {
dsn := d.config.GetPaths().DBPath dsn := d.config.GetPaths().DBPath
// Проверяем директорию для БД
dbDir := filepath.Dir(dsn)
if _, err := os.Stat(dbDir); err != nil {
if os.IsNotExist(err) {
// Директория не существует - пытаемся создать
if mkErr := os.MkdirAll(dbDir, 0775); mkErr != nil {
// Не смогли создать - вернём ошибку, пользователь должен использовать alr fix
return fmt.Errorf("cache directory does not exist, please run 'alr fix' to create it: %w", mkErr)
}
} else {
return fmt.Errorf("failed to check database directory: %w", err)
}
}
engine, err := xorm.NewEngine("sqlite", dsn) engine, err := xorm.NewEngine("sqlite", dsn)
// engine.SetLogLevel(log.LOG_DEBUG) // engine.SetLogLevel(log.LOG_DEBUG)
// engine.ShowSQL(true) // engine.ShowSQL(true)

View File

@@ -17,12 +17,9 @@
package utils package utils
import ( import (
"errors"
"os" "os"
"os/exec" "os/exec"
"os/user" "os/user"
"strconv"
"syscall"
"github.com/leonelquinteros/gotext" "github.com/leonelquinteros/gotext"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@@ -32,114 +29,12 @@ import (
"gitea.plemya-x.ru/Plemya-x/ALR/internal/constants" "gitea.plemya-x.ru/Plemya-x/ALR/internal/constants"
) )
func GetUidGidAlrUserString() (string, string, error) { // IsNotRoot проверяет, что текущий пользователь не является root
u, err := user.Lookup("alr")
if err != nil {
return "", "", err
}
return u.Uid, u.Gid, nil
}
func GetUidGidAlrUser() (int, int, error) {
strUid, strGid, err := GetUidGidAlrUserString()
if err != nil {
return 0, 0, err
}
uid, err := strconv.Atoi(strUid)
if err != nil {
return 0, 0, err
}
gid, err := strconv.Atoi(strGid)
if err != nil {
return 0, 0, err
}
return uid, gid, nil
}
func DropCapsToAlrUser() error {
uid, gid, err := GetUidGidAlrUser()
if err != nil {
return err
}
err = syscall.Setgid(gid)
if err != nil {
return err
}
err = syscall.Setuid(uid)
if err != nil {
return err
}
return EnsureIsAlrUser()
}
func ExitIfCantDropGidToAlr() cli.ExitCoder {
_, gid, err := GetUidGidAlrUser()
if err != nil {
return cliutils.FormatCliExit("cannot get gid alr", err)
}
err = syscall.Setgid(gid)
if err != nil {
return cliutils.FormatCliExit("cannot get setgid alr", err)
}
return nil
}
// ExitIfCantDropCapsToAlrUser attempts to drop capabilities to the already
// running user. Returns a cli.ExitCoder with an error if the operation fails.
// See also [ExitIfCantDropCapsToAlrUserNoPrivs] for a version that also applies
// no-new-privs.
func ExitIfCantDropCapsToAlrUser() cli.ExitCoder {
err := DropCapsToAlrUser()
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error on dropping capabilities"), err)
}
return nil
}
func ExitIfCantSetNoNewPrivs() cli.ExitCoder {
if err := NoNewPrivs(); err != nil {
return cliutils.FormatCliExit("error on NoNewPrivs", err)
}
return nil
}
// ExitIfCantDropCapsToAlrUserNoPrivs combines [ExitIfCantDropCapsToAlrUser] with [ExitIfCantSetNoNewPrivs]
func ExitIfCantDropCapsToAlrUserNoPrivs() cli.ExitCoder {
if err := ExitIfCantDropCapsToAlrUser(); err != nil {
return err
}
if err := ExitIfCantSetNoNewPrivs(); err != nil {
return err
}
return nil
}
func IsNotRoot() bool { func IsNotRoot() bool {
return os.Getuid() != 0 return os.Getuid() != 0
} }
func EnsureIsAlrUser() error { // EnuseIsPrivilegedGroupMember проверяет, что пользователь является членом привилегированной группы (wheel)
uid, gid, err := GetUidGidAlrUser()
if err != nil {
return err
}
newUid := syscall.Getuid()
if newUid != uid {
return errors.New("uid don't matches requested")
}
newGid := syscall.Getgid()
if newGid != gid {
return errors.New("gid don't matches requested")
}
return nil
}
func EnuseIsPrivilegedGroupMember() error { func EnuseIsPrivilegedGroupMember() error {
currentUser, err := user.Current() currentUser, err := user.Current()
if err != nil { if err != nil {
@@ -164,26 +59,6 @@ func EnuseIsPrivilegedGroupMember() error {
return cliutils.FormatCliExit(gotext.Get("You need to be a %s member to perform this action", constants.PrivilegedGroup), nil) return cliutils.FormatCliExit(gotext.Get("You need to be a %s member to perform this action", constants.PrivilegedGroup), nil)
} }
func EscalateToRootGid() error {
return syscall.Setgid(0)
}
func EscalateToRootUid() error {
return syscall.Setuid(0)
}
func EscalateToRoot() error {
err := EscalateToRootUid()
if err != nil {
return err
}
err = EscalateToRootGid()
if err != nil {
return err
}
return nil
}
func RootNeededAction(f cli.ActionFunc) cli.ActionFunc { func RootNeededAction(f cli.ActionFunc) cli.ActionFunc {
return func(ctx *cli.Context) error { return func(ctx *cli.Context) error {
deps, err := appbuilder. deps, err := appbuilder.

View File

@@ -16,8 +16,44 @@
package utils package utils
import "golang.org/x/sys/unix" import (
"os"
"os/exec"
"strings"
"golang.org/x/sys/unix"
)
func NoNewPrivs() error { func NoNewPrivs() error {
return unix.Prctl(unix.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) return unix.Prctl(unix.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)
} }
// EnsureTempDirWithRootOwner создает каталог в /tmp/alr с правами для группы wheel
// Все каталоги в /tmp/alr принадлежат root:wheel с правами 775
// Для других каталогов использует стандартные права
func EnsureTempDirWithRootOwner(path string, mode os.FileMode) error {
if strings.HasPrefix(path, "/tmp/alr") {
// Сначала создаем директорию обычным способом
err := os.MkdirAll(path, mode)
if err != nil {
return err
}
// Все каталоги в /tmp/alr доступны для группы wheel
// Устанавливаем setgid бит (2775), чтобы новые файлы наследовали группу
permissions := "2775"
group := "wheel"
// Устанавливаем права с setgid битом
err = exec.Command("sudo", "chmod", permissions, path).Run()
if err != nil {
return err
}
// Устанавливаем владельца root:wheel
return exec.Command("sudo", "chown", "root:"+group, path).Run()
}
// Для остальных каталогов обычное создание
return os.MkdirAll(path, mode)
}

View File

@@ -35,7 +35,6 @@ import (
appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder" appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager" "gitea.plemya-x.ru/Plemya-x/ALR/internal/manager"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides" "gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
) )
@@ -60,9 +59,6 @@ func ListCmd() *cli.Command {
}, },
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil {
return err
}
ctx := c.Context ctx := c.Context

View File

@@ -87,7 +87,6 @@ func GetApp() *cli.App {
// Internal commands // Internal commands
InternalBuildCmd(), InternalBuildCmd(),
InternalInstallCmd(), InternalInstallCmd(),
InternalMountCmd(),
InternalReposCmd(), InternalReposCmd(),
}, },
Before: func(c *cli.Context) error { Before: func(c *cli.Context) error {

View File

@@ -1,3 +1,19 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// DO NOT EDIT MANUALLY. This file is generated. // DO NOT EDIT MANUALLY. This file is generated.
package alrsh package alrsh

View File

@@ -280,14 +280,14 @@ func handleCache(cacheDir, dest, name string, t Type) (bool, error) {
cd.Close() cd.Close()
if slices.Contains(names, name) { if slices.Contains(names, name) {
err = os.Link(filepath.Join(cacheDir, name), dest) err = linkOrCopy(filepath.Join(cacheDir, name), dest)
if err != nil { if err != nil {
return false, err return false, err
} }
return true, nil return true, nil
} }
case TypeDir: case TypeDir:
err := linkDir(cacheDir, dest) err := linkOrCopyDir(cacheDir, dest)
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -296,8 +296,40 @@ func handleCache(cacheDir, dest, name string, t Type) (bool, error) {
return false, nil return false, nil
} }
// Функция linkDir рекурсивно создает жесткие ссылки для файлов из каталога src в каталог dest // linkOrCopy пытается создать жесткую ссылку, а если не получается - копирует файл
func linkDir(src, dest string) error { func linkOrCopy(src, dest string) error {
err := os.Link(src, dest)
if err != nil {
// Если не удалось создать ссылку, копируем файл
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
destFile, err := os.Create(dest)
if err != nil {
return err
}
defer destFile.Close()
_, err = io.Copy(destFile, srcFile)
if err != nil {
return err
}
// Копируем права доступа
srcInfo, err := srcFile.Stat()
if err != nil {
return err
}
return os.Chmod(dest, srcInfo.Mode())
}
return nil
}
// linkOrCopyDir рекурсивно создает жесткие ссылки или копирует файлы из каталога src в каталог dest
func linkOrCopyDir(src, dest string) error {
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return err return err
@@ -317,7 +349,7 @@ func linkDir(src, dest string) error {
return os.MkdirAll(newPath, info.Mode()) return os.MkdirAll(newPath, info.Mode())
} }
return os.Link(path, newPath) return linkOrCopy(path, newPath)
}) })
} }

View File

@@ -25,6 +25,7 @@ import (
"path/filepath" "path/filepath"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/config" "gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
) )
type Config interface { type Config interface {
@@ -61,7 +62,8 @@ func (dc *DownloadCache) New(ctx context.Context, id string) (string, error) {
} }
} }
err = os.MkdirAll(itemPath, 0o755) // Используем специальную функцию для создания каталогов
err = utils.EnsureTempDirWithRootOwner(itemPath, 0o755)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@@ -21,7 +21,6 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder" appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
) )
func RefreshCmd() *cli.Command { func RefreshCmd() *cli.Command {
@@ -30,9 +29,6 @@ func RefreshCmd() *cli.Command {
Usage: gotext.Get("Pull all repositories that have changed"), Usage: gotext.Get("Pull all repositories that have changed"),
Aliases: []string{"ref"}, Aliases: []string{"ref"},
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
return err
}
ctx := c.Context ctx := c.Context

View File

@@ -114,9 +114,6 @@ func RemoveRepoCmd() *cli.Command {
return cliutils.FormatCliExit(gotext.Get("Error saving config"), err) return cliutils.FormatCliExit(gotext.Get("Error saving config"), err)
} }
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
return err
}
deps, err = appbuilder. deps, err = appbuilder.
New(ctx). New(ctx).

View File

@@ -29,7 +29,6 @@ import (
appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder" appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides" "gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/search" "gitea.plemya-x.ru/Plemya-x/ALR/internal/search"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
) )
@@ -72,9 +71,6 @@ func SearchCmd() *cli.Command {
}, },
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil {
return err
}
ctx := c.Context ctx := c.Context

View File

@@ -55,9 +55,6 @@ func UpgradeCmd() *cli.Command {
}, },
}, },
Action: utils.RootNeededAction(func(c *cli.Context) error { Action: utils.RootNeededAction(func(c *cli.Context) error {
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
return err
}
installer, installerClose, err := build.GetSafeInstaller() installer, installerClose, err := build.GetSafeInstaller()
if err != nil { if err != nil {
@@ -65,9 +62,6 @@ func UpgradeCmd() *cli.Command {
} }
defer installerClose() defer installerClose()
if err := utils.ExitIfCantSetNoNewPrivs(); err != nil {
return err
}
scripter, scripterClose, err := build.GetSafeScriptExecutor() scripter, scripterClose, err := build.GetSafeScriptExecutor()
if err != nil { if err != nil {