diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index 8948d66..0e3fd2a 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -47,7 +47,7 @@ jobs: - name: Prepare for install run: | - apt-get update && apt-get install -y libcap2-bin bindfs + apt-get update - name: Build alr env: diff --git a/.gitignore b/.gitignore index a1536ac..6b4d30c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,11 +3,12 @@ /cmd/alr-api-server/alr-api-server /dist/ /internal/config/version.txt -.fleet -.idea -.gigaide +.fleet/ +.idea/ +.gigaide/ *.out e2e-tests/alr +CLAUDE.md commit_msg.txt \ No newline at end of file diff --git a/Makefile b/Makefile index 4057e52..68fa3f7 100644 --- a/Makefile +++ b/Makefile @@ -49,17 +49,12 @@ install: \ $(INSTALLED_BIN): $(BIN) install -Dm755 $< $@ 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 \ - install -d -o alr -g alr -m 755 $$dir; \ + install -d -m 775 $$dir; \ + chgrp wheel $$dir; \ done else - @echo "Skipping user and root dir creation (CREATE_SYSTEM_RESOURCES=0)" + @echo "Skipping root dir creation (CREATE_SYSTEM_RESOURCES=0)" endif $(INSTALLED_BASH_COMPLETION): $(BASH_COMPLETION) diff --git a/build.go b/build.go index 166f533..a74117d 100644 --- a/build.go +++ b/build.go @@ -72,12 +72,6 @@ func BuildCmd() *cli.Command { 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 deps, err := appbuilder. @@ -156,19 +150,9 @@ func BuildCmd() *cli.Command { 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() if err != nil { @@ -176,9 +160,7 @@ func BuildCmd() *cli.Command { } defer installerClose() - if err := utils.ExitIfCantSetNoNewPrivs(); err != nil { - return err - } + scripter, scripterClose, err := build.GetSafeScriptExecutor() if err != nil { diff --git a/fix.go b/fix.go index 94f25c5..3872d18 100644 --- a/fix.go +++ b/fix.go @@ -23,6 +23,7 @@ import ( "io/fs" "log/slog" "os" + "os/exec" "path/filepath" "github.com/leonelquinteros/gotext" @@ -38,9 +39,8 @@ func FixCmd() *cli.Command { Name: "fix", Usage: gotext.Get("Attempt to fix problems with ALR"), Action: func(c *cli.Context) error { - if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil { - return err - } + // Команда выполняется от текущего пользователя + // При необходимости будет запрошен sudo для удаления файлов root ctx := c.Context @@ -57,37 +57,126 @@ func FixCmd() *cli.Command { 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) if err != nil { - return cliutils.FormatCliExit(gotext.Get("Unable to open cache directory"), err) - } - defer dir.Close() + if os.IsNotExist(err) { + // Директория не существует, просто создадим её позже + 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) - if err != nil { - 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) + entries, err := dir.Readdirnames(-1) + if err != nil { + return cliutils.FormatCliExit(gotext.Get("Unable to read cache directory contents"), 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 { - 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")) - err = os.MkdirAll(paths.CacheDir, 0o755) + // Пробуем создать директорию кэша + err = os.MkdirAll(paths.CacheDir, 0o775) 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. diff --git a/info.go b/info.go index 31e6c9f..e753fbc 100644 --- a/info.go +++ b/info.go @@ -31,7 +31,6 @@ import ( "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils" 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/utils" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh" "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 { - if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil { - return err - } ctx := c.Context deps, err := appbuilder. @@ -74,9 +70,7 @@ func InfoCmd() *cli.Command { return nil }), Action: func(c *cli.Context) error { - if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil { - return err - } + // Запуск от текущего пользователя args := c.Args() if args.Len() < 1 { diff --git a/install.go b/install.go index 5385437..abdfdc4 100644 --- a/install.go +++ b/install.go @@ -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) } - if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil { - return err - } installer, installerClose, err := build.GetSafeInstaller() if err != nil { @@ -61,9 +58,6 @@ func InstallCmd() *cli.Command { } defer installerClose() - if err := utils.ExitIfCantSetNoNewPrivs(); err != nil { - return err - } scripter, scripterClose, err := build.GetSafeScriptExecutor() if err != nil { @@ -116,9 +110,6 @@ func InstallCmd() *cli.Command { return nil }), BashComplete: cliutils.BashCompleteWithError(func(c *cli.Context) error { - if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil { - return err - } ctx := c.Context deps, err := appbuilder. diff --git a/internal.go b/internal.go index 3080540..eeec206 100644 --- a/internal.go +++ b/internal.go @@ -17,14 +17,8 @@ package main import ( - "bufio" - "errors" - "fmt" "log/slog" "os" - "os/exec" - "os/user" - "path/filepath" "syscall" "github.com/hashicorp/go-hclog" @@ -36,7 +30,6 @@ import ( "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils" 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/constants" "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/utils" @@ -52,9 +45,6 @@ func InternalBuildCmd() *cli.Command { slog.Debug("start _internal-safe-script-executor", "uid", syscall.Getuid(), "gid", syscall.Getgid()) - if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil { - return err - } cfg := config.New() err := cfg.Load() @@ -92,9 +82,6 @@ func InternalReposCmd() *cli.Command { Action: utils.RootNeededAction(func(ctx *cli.Context) error { logger.SetupForGoPlugin() - if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil { - return err - } deps, err := appbuilder. New(ctx.Context). @@ -129,16 +116,7 @@ func InternalInstallCmd() *cli.Command { Action: func(c *cli.Context) error { logger.SetupForGoPlugin() - if err := utils.EnsureIsAlrUser(); err != nil { - 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) - } + // Запуск от текущего пользователя, повышение прав будет через sudo при необходимости deps, err := appbuilder. 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 - }, - } -} diff --git a/internal/build/plugins.go b/internal/build/plugins.go index 75e6f3a..685dd1f 100644 --- a/internal/build/plugins.go +++ b/internal/build/plugins.go @@ -44,9 +44,9 @@ var HandshakeConfig = plugin.HandshakeConfig{ func setCommonCmdEnv(cmd *exec.Cmd) { cmd.Env = []string{ - "HOME=/var/cache/alr", - "LOGNAME=alr", - "USER=alr", + "HOME=" + os.Getenv("HOME"), + "LOGNAME=" + os.Getenv("USER"), + "USER=" + os.Getenv("USER"), "PATH=/usr/bin:/bin:/usr/local/bin", } for _, env := range os.Environ() { @@ -102,9 +102,7 @@ func getSafeExecutor[T any](subCommand, pluginName string) (T, func(), error) { Cmd: cmd, Logger: logger.GetHCLoggerAdapter(), SkipHostEnv: true, - UnixSocketConfig: &plugin.UnixSocketConfig{ - Group: "alr", - }, + UnixSocketConfig: &plugin.UnixSocketConfig{}, SyncStderr: os.Stderr, }) rpcClient, err := client.Client() diff --git a/internal/build/source_downloader.go b/internal/build/source_downloader.go index bf5f9d6..471d51a 100644 --- a/internal/build/source_downloader.go +++ b/internal/build/source_downloader.go @@ -23,6 +23,7 @@ import ( "os" "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/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) if err != nil { diff --git a/internal/build/utils.go b/internal/build/utils.go index 871b724..b5bb92f 100644 --- a/internal/build/utils.go +++ b/internal/build/utils.go @@ -40,6 +40,7 @@ import ( "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/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/distro" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" @@ -47,15 +48,21 @@ import ( // Функция prepareDirs подготавливает директории для сборки. 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 { return err } - err = os.MkdirAll(dirs.SrcDir, 0o755) // Создаем директорию для источников - if err != nil { - return err - } - return os.MkdirAll(dirs.PkgDir, 0o755) // Создаем директорию для пакетов + + // Создаем директорию для пакетов с setgid битом + return utils.EnsureTempDirWithRootOwner(dirs.PkgDir, 0o2775) } // Функция buildContents создает секцию содержимого пакета, которая содержит файлы, diff --git a/internal/config/config.go b/internal/config/config.go index 902b07d..ef12d83 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -21,6 +21,7 @@ package config import ( "fmt" + "os" "path/filepath" "github.com/goccy/go-yaml" @@ -55,7 +56,12 @@ func defaultConfigKoanf() *koanf.Koanf { "ignorePkgUpdates": []string{}, "logLevel": "info", "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 { panic(k) @@ -98,8 +104,15 @@ func (c *ALRConfig) Load() error { c.paths.UserConfigPath = constants.SystemConfigPath c.paths.CacheDir = constants.SystemCachePath c.paths.RepoDir = filepath.Join(c.paths.CacheDir, "repo") - c.paths.PkgsDir = filepath.Join(c.paths.CacheDir, "pkgs") - c.paths.DBPath = filepath.Join(c.paths.CacheDir, "db") + c.paths.PkgsDir = filepath.Join(constants.TempDir, "pkgs") // Перемещаем в /tmp/alr/pkgs + 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 } diff --git a/internal/constants/constants.go b/internal/constants/constants.go index d16d2b9..5223368 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -19,6 +19,6 @@ package constants const ( SystemConfigPath = "/etc/alr/alr.toml" SystemCachePath = "/var/cache/alr" - AlrRunDir = "/var/run/alr" + TempDir = "/tmp/alr" PrivilegedGroup = "wheel" ) diff --git a/internal/db/db.go b/internal/db/db.go index ffb9298..8b5e529 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -21,7 +21,10 @@ package db import ( "context" + "fmt" "log/slog" + "os" + "path/filepath" "github.com/leonelquinteros/gotext" _ "modernc.org/sqlite" @@ -54,6 +57,21 @@ func New(config Config) *Database { func (d *Database) Connect() error { 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.SetLogLevel(log.LOG_DEBUG) // engine.ShowSQL(true) diff --git a/internal/utils/cmd.go b/internal/utils/cmd.go index 3b4025f..b604095 100644 --- a/internal/utils/cmd.go +++ b/internal/utils/cmd.go @@ -17,12 +17,9 @@ package utils import ( - "errors" "os" "os/exec" "os/user" - "strconv" - "syscall" "github.com/leonelquinteros/gotext" "github.com/urfave/cli/v2" @@ -32,114 +29,12 @@ import ( "gitea.plemya-x.ru/Plemya-x/ALR/internal/constants" ) -func GetUidGidAlrUserString() (string, string, error) { - 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 -} - +// IsNotRoot проверяет, что текущий пользователь не является root func IsNotRoot() bool { return os.Getuid() != 0 } -func EnsureIsAlrUser() error { - 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 -} - +// EnuseIsPrivilegedGroupMember проверяет, что пользователь является членом привилегированной группы (wheel) func EnuseIsPrivilegedGroupMember() error { currentUser, err := user.Current() 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) } -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 { return func(ctx *cli.Context) error { deps, err := appbuilder. diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 8ccb66b..f78e09c 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -16,8 +16,44 @@ package utils -import "golang.org/x/sys/unix" +import ( + "os" + "os/exec" + "strings" + + "golang.org/x/sys/unix" +) func NoNewPrivs() error { 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) +} diff --git a/list.go b/list.go index b3a0dbb..26aa023 100644 --- a/list.go +++ b/list.go @@ -35,7 +35,6 @@ import ( 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/overrides" - "gitea.plemya-x.ru/Plemya-x/ALR/internal/utils" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh" ) @@ -60,9 +59,6 @@ func ListCmd() *cli.Command { }, }, Action: func(c *cli.Context) error { - if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil { - return err - } ctx := c.Context diff --git a/main.go b/main.go index 368ec3d..c7d5763 100644 --- a/main.go +++ b/main.go @@ -87,7 +87,6 @@ func GetApp() *cli.App { // Internal commands InternalBuildCmd(), InternalInstallCmd(), - InternalMountCmd(), InternalReposCmd(), }, Before: func(c *cli.Context) error { diff --git a/pkg/alrsh/package_gen.go b/pkg/alrsh/package_gen.go index 0fca143..834deb8 100644 --- a/pkg/alrsh/package_gen.go +++ b/pkg/alrsh/package_gen.go @@ -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 . + // DO NOT EDIT MANUALLY. This file is generated. package alrsh diff --git a/pkg/dl/dl.go b/pkg/dl/dl.go index 71ceade..db41c3a 100644 --- a/pkg/dl/dl.go +++ b/pkg/dl/dl.go @@ -280,14 +280,14 @@ func handleCache(cacheDir, dest, name string, t Type) (bool, error) { cd.Close() if slices.Contains(names, name) { - err = os.Link(filepath.Join(cacheDir, name), dest) + err = linkOrCopy(filepath.Join(cacheDir, name), dest) if err != nil { return false, err } return true, nil } case TypeDir: - err := linkDir(cacheDir, dest) + err := linkOrCopyDir(cacheDir, dest) if err != nil { return false, err } @@ -296,8 +296,40 @@ func handleCache(cacheDir, dest, name string, t Type) (bool, error) { return false, nil } -// Функция linkDir рекурсивно создает жесткие ссылки для файлов из каталога src в каталог dest -func linkDir(src, dest string) error { +// linkOrCopy пытается создать жесткую ссылку, а если не получается - копирует файл +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 { if err != nil { return err @@ -317,7 +349,7 @@ func linkDir(src, dest string) error { return os.MkdirAll(newPath, info.Mode()) } - return os.Link(path, newPath) + return linkOrCopy(path, newPath) }) } diff --git a/pkg/dlcache/dlcache.go b/pkg/dlcache/dlcache.go index 2462f68..5207d37 100644 --- a/pkg/dlcache/dlcache.go +++ b/pkg/dlcache/dlcache.go @@ -25,6 +25,7 @@ import ( "path/filepath" "gitea.plemya-x.ru/Plemya-x/ALR/internal/config" + "gitea.plemya-x.ru/Plemya-x/ALR/internal/utils" ) 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 { return "", err } diff --git a/refresh.go b/refresh.go index 6b031dd..9d5782a 100644 --- a/refresh.go +++ b/refresh.go @@ -21,7 +21,6 @@ import ( "github.com/urfave/cli/v2" 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 { @@ -30,9 +29,6 @@ func RefreshCmd() *cli.Command { Usage: gotext.Get("Pull all repositories that have changed"), Aliases: []string{"ref"}, Action: func(c *cli.Context) error { - if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil { - return err - } ctx := c.Context diff --git a/repo.go b/repo.go index 5669e5f..5e6a178 100644 --- a/repo.go +++ b/repo.go @@ -114,9 +114,6 @@ func RemoveRepoCmd() *cli.Command { return cliutils.FormatCliExit(gotext.Get("Error saving config"), err) } - if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil { - return err - } deps, err = appbuilder. New(ctx). diff --git a/search.go b/search.go index 02c88ae..0a75b6d 100644 --- a/search.go +++ b/search.go @@ -29,7 +29,6 @@ import ( 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/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/distro" ) @@ -72,9 +71,6 @@ func SearchCmd() *cli.Command { }, }, Action: func(c *cli.Context) error { - if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil { - return err - } ctx := c.Context diff --git a/upgrade.go b/upgrade.go index 254edaa..2d67d68 100644 --- a/upgrade.go +++ b/upgrade.go @@ -55,9 +55,6 @@ func UpgradeCmd() *cli.Command { }, }, Action: utils.RootNeededAction(func(c *cli.Context) error { - if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil { - return err - } installer, installerClose, err := build.GetSafeInstaller() if err != nil { @@ -65,9 +62,6 @@ func UpgradeCmd() *cli.Command { } defer installerClose() - if err := utils.ExitIfCantSetNoNewPrivs(); err != nil { - return err - } scripter, scripterClose, err := build.GetSafeScriptExecutor() if err != nil {