4 Commits

Author SHA1 Message Date
6773d51caf Добавление функций обработки files
All checks were successful
Pre-commit / pre-commit (push) Successful in 5m8s
Create Release / changelog (push) Successful in 3m5s
2025-09-21 16:42:04 +03:00
4a616f2137 Исправление функционала создания дирректорий для работы ALR
All checks were successful
Pre-commit / pre-commit (push) Successful in 5m23s
Create Release / changelog (push) Successful in 3m3s
2025-09-21 16:21:23 +03:00
9efebbc02a Исправление функционала создания дирректорий для работы ALR
All checks were successful
Pre-commit / pre-commit (push) Successful in 5m11s
Create Release / changelog (push) Successful in 3m11s
2025-09-21 15:31:51 +03:00
ef41d682a1 Исправление функционала повышения привилегий
All checks were successful
Pre-commit / pre-commit (push) Successful in 5m12s
Create Release / changelog (push) Successful in 3m8s
2025-09-21 15:04:42 +03:00
12 changed files with 534 additions and 109 deletions

View File

@@ -63,7 +63,7 @@ func BuildCmd() *cli.Command {
}, },
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
if err := utils.EnuseIsPrivilegedGroupMember(); err != nil { if err := utils.CheckUserPrivileges(); err != nil {
return err return err
} }

View File

@@ -45,17 +45,17 @@ func TestE2EIssue130Install(t *testing.T) {
) )
runMatrixSuite( runMatrixSuite(
t, t,
"alr install {package}+alr-{repo}", "alr install {package}+{repo}",
COMMON_SYSTEMS, COMMON_SYSTEMS,
func(t *testing.T, r capytest.Runner) { func(t *testing.T, r capytest.Runner) {
t.Parallel() t.Parallel()
defaultPrepare(t, r) defaultPrepare(t, r)
r.Command("sudo", "alr", "in", fmt.Sprintf("foo-pkg+alr-%s", REPO_NAME_FOR_E2E_TESTS)). r.Command("sudo", "alr", "in", fmt.Sprintf("foo-pkg+%s", REPO_NAME_FOR_E2E_TESTS)).
ExpectSuccess(). ExpectSuccess().
Run(t) Run(t)
r.Command("sudo", "alr", "in", fmt.Sprintf("bar-pkg+alr-%s", "NOT_REPO_NAME_FOR_E2E_TESTS")). r.Command("sudo", "alr", "in", fmt.Sprintf("bar-pkg+%s", "NOT_REPO_NAME_FOR_E2E_TESTS")).
ExpectFailure(). ExpectFailure().
Run(t) Run(t)
}, },

34
fix.go
View File

@@ -131,22 +131,22 @@ func FixCmd() *cli.Command {
} }
} }
// Создаем базовый каталог /tmp/alr с владельцем root:wheel и правами 775 // Создаем базовый каталог /tmp/alr с владельцем root:wheel и правами 2775
err = utils.EnsureTempDirWithRootOwner(tmpDir, 0o775) err = utils.EnsureTempDirWithRootOwner(tmpDir, 0o2775)
if err != nil { if err != nil {
slog.Warn(gotext.Get("Unable to create temporary directory"), "error", err) slog.Warn(gotext.Get("Unable to create temporary directory"), "error", err)
} }
// Создаем каталог dl с правами для группы wheel // Создаем каталог dl с правами для группы wheel
dlDir := filepath.Join(tmpDir, "dl") dlDir := filepath.Join(tmpDir, "dl")
err = utils.EnsureTempDirWithRootOwner(dlDir, 0o775) err = utils.EnsureTempDirWithRootOwner(dlDir, 0o2775)
if err != nil { if err != nil {
slog.Warn(gotext.Get("Unable to create download directory"), "error", err) slog.Warn(gotext.Get("Unable to create download directory"), "error", err)
} }
// Создаем каталог pkgs с правами для группы wheel // Создаем каталог pkgs с правами для группы wheel
pkgsDir := filepath.Join(tmpDir, "pkgs") pkgsDir := filepath.Join(tmpDir, "pkgs")
err = utils.EnsureTempDirWithRootOwner(pkgsDir, 0o775) err = utils.EnsureTempDirWithRootOwner(pkgsDir, 0o2775)
if err != nil { if err != nil {
slog.Warn(gotext.Get("Unable to create packages directory"), "error", err) slog.Warn(gotext.Get("Unable to create packages directory"), "error", err)
} }
@@ -158,7 +158,8 @@ func FixCmd() *cli.Command {
// Проверяем, есть ли файлы в директории // Проверяем, есть ли файлы в директории
entries, err := os.ReadDir(tmpDir) entries, err := os.ReadDir(tmpDir)
if err == nil && len(entries) > 0 { if err == nil && len(entries) > 0 {
fixCmd := execWithPrivileges("chown", "-R", "root:wheel", tmpDir) group := utils.GetPrivilegedGroup()
fixCmd := execWithPrivileges("chown", "-R", "root:"+group, tmpDir)
if fixErr := fixCmd.Run(); fixErr != nil { if fixErr := fixCmd.Run(); fixErr != nil {
slog.Warn(gotext.Get("Unable to fix file ownership"), "error", fixErr) slog.Warn(gotext.Get("Unable to fix file ownership"), "error", fixErr)
} }
@@ -172,26 +173,11 @@ func FixCmd() *cli.Command {
slog.Info(gotext.Get("Rebuilding cache")) slog.Info(gotext.Get("Rebuilding cache"))
// Пробуем создать директорию кэша // Создаем директорию кэша с правильными правами
err = os.MkdirAll(paths.CacheDir, 0o775) slog.Info(gotext.Get("Creating cache directory"))
err = utils.EnsureTempDirWithRootOwner(paths.CacheDir, 0o2775)
if err != nil { if err != nil {
// Если не получилось, пробуем через sudo с правильными правами для группы wheel return cliutils.FormatCliExit(gotext.Get("Unable to create new cache directory"), err)
slog.Info(gotext.Get("Creating cache directory with sudo"))
sudoCmd := execWithPrivileges("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 := execWithPrivileges("chmod", "775", paths.CacheDir)
if chmodErr := chmodCmd.Run(); chmodErr != nil {
return cliutils.FormatCliExit(gotext.Get("Unable to set cache directory permissions"), chmodErr)
}
chgrpCmd := execWithPrivileges("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

@@ -56,13 +56,19 @@ func prepareDirs(dirs types.Directories) error {
// Новые директории будут созданы или перезаписаны // Новые директории будут созданы или перезаписаны
slog.Debug("Failed to remove base directory", "path", dirs.BaseDir, "error", err) slog.Debug("Failed to remove base directory", "path", dirs.BaseDir, "error", err)
} }
// Создаем базовую директорию для пакета с setgid битом
err = utils.EnsureTempDirWithRootOwner(dirs.BaseDir, 0o2775)
if err != nil {
return err
}
// Создаем директории с правильным владельцем для /tmp/alr с setgid битом // Создаем директории с правильным владельцем для /tmp/alr с setgid битом
err = utils.EnsureTempDirWithRootOwner(dirs.SrcDir, 0o2775) err = utils.EnsureTempDirWithRootOwner(dirs.SrcDir, 0o2775)
if err != nil { if err != nil {
return err return err
} }
// Создаем директорию для пакетов с setgid битом // Создаем директорию для пакетов с setgid битом
return utils.EnsureTempDirWithRootOwner(dirs.PkgDir, 0o2775) return utils.EnsureTempDirWithRootOwner(dirs.PkgDir, 0o2775)
} }
@@ -169,7 +175,7 @@ func normalizeContents(contents []*files.Content) {
} }
} }
var RegexpALRPackageName = regexp.MustCompile(`^(?P<package>[^+]+)\+alr-(?P<repo>.+)$`) var RegexpALRPackageName = regexp.MustCompile(`^(?P<package>[^+]+)\+(?P<repo>.+)$`)
func getBasePkgInfo(vars *alrsh.Package, input interface { func getBasePkgInfo(vars *alrsh.Package, input interface {
RepositoryProvider RepositoryProvider
@@ -177,12 +183,8 @@ func getBasePkgInfo(vars *alrsh.Package, input interface {
}, },
) *nfpm.Info { ) *nfpm.Info {
repo := input.Repository() repo := input.Repository()
// Избегаем дублирования "alr-" префикса
if strings.HasPrefix(repo, "alr-") {
repo = repo[4:] // убираем "alr-" префикс
}
return &nfpm.Info{ return &nfpm.Info{
Name: fmt.Sprintf("%s+alr-%s", vars.Name, repo), Name: fmt.Sprintf("%s+%s", vars.Name, repo),
Arch: cpu.Arch(), Arch: cpu.Arch(),
Version: vars.Version, Version: vars.Version,
Release: overrides.ReleasePlatformSpecific(vars.Release, input.OSRelease()), Release: overrides.ReleasePlatformSpecific(vars.Release, input.OSRelease()),

View File

@@ -0,0 +1,158 @@
// 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/>.
package build
import (
"testing"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
)
type mockInput struct {
repo string
osInfo *distro.OSRelease
}
func (m *mockInput) Repository() string {
return m.repo
}
func (m *mockInput) OSRelease() *distro.OSRelease {
return m.osInfo
}
func TestGetBasePkgInfo(t *testing.T) {
tests := []struct {
name string
packageName string
repoName string
expectedName string
}{
{
name: "обычный репозиторий",
packageName: "test-package",
repoName: "default",
expectedName: "test-package+default",
},
{
name: "репозиторий с alr- префиксом",
packageName: "test-package",
repoName: "alr-default",
expectedName: "test-package+alr-default",
},
{
name: "репозиторий с двойным alr- префиксом",
packageName: "test-package",
repoName: "alr-alr-repo",
expectedName: "test-package+alr-alr-repo",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pkg := &alrsh.Package{
Name: tt.packageName,
Version: "1.0.0",
Release: 1,
}
input := &mockInput{
repo: tt.repoName,
osInfo: &distro.OSRelease{
ID: "test",
},
}
info := getBasePkgInfo(pkg, input)
if info.Name != tt.expectedName {
t.Errorf("getBasePkgInfo() имя пакета = %v, ожидается %v", info.Name, tt.expectedName)
}
})
}
}
func TestRegexpALRPackageName(t *testing.T) {
tests := []struct {
name string
packageName string
expectedPkg string
expectedRepo string
shouldMatch bool
}{
{
name: "новый формат - обычный репозиторий",
packageName: "test-package+default",
expectedPkg: "test-package",
expectedRepo: "default",
shouldMatch: true,
},
{
name: "новый формат - alr-default репозиторий",
packageName: "test-package+alr-default",
expectedPkg: "test-package",
expectedRepo: "alr-default",
shouldMatch: true,
},
{
name: "новый формат - двойной alr- префикс",
packageName: "test-package+alr-alr-repo",
expectedPkg: "test-package",
expectedRepo: "alr-alr-repo",
shouldMatch: true,
},
{
name: "некорректный формат - без плюса",
packageName: "test-package",
shouldMatch: false,
},
{
name: "некорректный формат - пустое имя пакета",
packageName: "+repo",
shouldMatch: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
matches := RegexpALRPackageName.FindStringSubmatch(tt.packageName)
if tt.shouldMatch {
if matches == nil {
t.Errorf("RegexpALRPackageName должен найти совпадение для %q", tt.packageName)
return
}
packageName := matches[RegexpALRPackageName.SubexpIndex("package")]
repoName := matches[RegexpALRPackageName.SubexpIndex("repo")]
if packageName != tt.expectedPkg {
t.Errorf("RegexpALRPackageName извлеченное имя пакета = %v, ожидается %v", packageName, tt.expectedPkg)
}
if repoName != tt.expectedRepo {
t.Errorf("RegexpALRPackageName извлеченное имя репозитория = %v, ожидается %v", repoName, tt.expectedRepo)
}
} else {
if matches != nil {
t.Errorf("RegexpALRPackageName не должен найти совпадение для %q", tt.packageName)
}
}
})
}
}

View File

@@ -62,11 +62,9 @@ func (d *Database) Connect() error {
dbDir := filepath.Dir(dsn) dbDir := filepath.Dir(dsn)
if _, err := os.Stat(dbDir); err != nil { if _, err := os.Stat(dbDir); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
// Директория не существует - пытаемся создать // Директория не существует - не пытаемся создать
if mkErr := os.MkdirAll(dbDir, 0775); mkErr != nil { // Пользователь должен использовать alr fix для создания системных каталогов
// Не смогли создать - вернём ошибку, пользователь должен использовать alr fix return fmt.Errorf("cache directory does not exist, please run 'sudo alr fix' to create it")
return fmt.Errorf("cache directory does not exist, please run 'alr fix' to create it: %w", mkErr)
}
} else { } else {
return fmt.Errorf("failed to check database directory: %w", err) return fmt.Errorf("failed to check database directory: %w", err)
} }

View File

@@ -47,9 +47,9 @@ func (rs *Repos) FindPkgs(ctx context.Context, pkgs []string) (map[string][]alrs
name := parts[1] name := parts[1]
result, err = rs.db.GetPkgs(ctx, "name = ? AND repository = ?", name, repo) result, err = rs.db.GetPkgs(ctx, "name = ? AND repository = ?", name, repo)
case strings.Contains(pkgName, "+alr-"): case strings.Contains(pkgName, "+"):
// pkg+alr-repo // pkg+repo
parts := strings.SplitN(pkgName, "+alr-", 2) parts := strings.SplitN(pkgName, "+", 2)
name := parts[0] name := parts[0]
repo := parts[1] repo := parts[1]
result, err = rs.db.GetPkgs(ctx, "name = ? AND repository = ?", name, repo) result, err = rs.db.GetPkgs(ctx, "name = ? AND repository = ?", name, repo)

View File

@@ -177,3 +177,228 @@ func filesFindCmd(hc interp.HandlerContext, cmd string, args []string) error {
return outputFiles(hc, foundFiles) return outputFiles(hc, foundFiles)
} }
func filesFindBinCmd(hc interp.HandlerContext, cmd string, args []string) error {
namePattern := "*"
if len(args) > 0 {
namePattern = args[0]
}
binPath := "./usr/bin/"
realPath := path.Join(hc.Dir, binPath)
if err := validateDir(realPath, "files-find-bin"); err != nil {
return err
}
var binFiles []string
err := filepath.Walk(realPath, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && matchNamePattern(info.Name(), namePattern) {
relPath, relErr := makeRelativePath(hc.Dir, p)
if relErr != nil {
return relErr
}
binFiles = append(binFiles, relPath)
}
return nil
})
if err != nil {
return fmt.Errorf("files-find-bin: %w", err)
}
return outputFiles(hc, binFiles)
}
func filesFindLibCmd(hc interp.HandlerContext, cmd string, args []string) error {
namePattern := "*"
if len(args) > 0 {
namePattern = args[0]
}
libPaths := []string{"./usr/lib/", "./usr/lib64/"}
var libFiles []string
for _, libPath := range libPaths {
realPath := path.Join(hc.Dir, libPath)
if _, err := os.Stat(realPath); os.IsNotExist(err) {
continue
}
err := filepath.Walk(realPath, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && matchNamePattern(info.Name(), namePattern) {
relPath, relErr := makeRelativePath(hc.Dir, p)
if relErr != nil {
return relErr
}
libFiles = append(libFiles, relPath)
}
return nil
})
if err != nil {
return fmt.Errorf("files-find-lib: %w", err)
}
}
return outputFiles(hc, libFiles)
}
func filesFindIncludeCmd(hc interp.HandlerContext, cmd string, args []string) error {
namePattern := "*"
if len(args) > 0 {
namePattern = args[0]
}
includePath := "./usr/include/"
realPath := path.Join(hc.Dir, includePath)
if err := validateDir(realPath, "files-find-include"); err != nil {
return err
}
var includeFiles []string
err := filepath.Walk(realPath, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && matchNamePattern(info.Name(), namePattern) {
relPath, relErr := makeRelativePath(hc.Dir, p)
if relErr != nil {
return relErr
}
includeFiles = append(includeFiles, relPath)
}
return nil
})
if err != nil {
return fmt.Errorf("files-find-include: %w", err)
}
return outputFiles(hc, includeFiles)
}
func filesFindShareCmd(hc interp.HandlerContext, cmd string, args []string) error {
namePattern := "*"
sharePath := "./usr/share/"
if len(args) > 0 {
if len(args) == 1 {
sharePath = "./usr/share/" + args[0] + "/"
} else {
sharePath = "./usr/share/" + args[0] + "/"
namePattern = args[1]
}
}
realPath := path.Join(hc.Dir, sharePath)
if err := validateDir(realPath, "files-find-share"); err != nil {
return err
}
var shareFiles []string
err := filepath.Walk(realPath, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && matchNamePattern(info.Name(), namePattern) {
relPath, relErr := makeRelativePath(hc.Dir, p)
if relErr != nil {
return relErr
}
shareFiles = append(shareFiles, relPath)
}
return nil
})
if err != nil {
return fmt.Errorf("files-find-share: %w", err)
}
return outputFiles(hc, shareFiles)
}
func filesFindManCmd(hc interp.HandlerContext, cmd string, args []string) error {
namePattern := "*"
manSection := "*"
if len(args) > 0 {
if len(args) == 1 {
manSection = args[0]
} else {
manSection = args[0]
namePattern = args[1]
}
}
manPath := "./usr/share/man/man" + manSection + "/"
realPath := path.Join(hc.Dir, manPath)
if err := validateDir(realPath, "files-find-man"); err != nil {
return err
}
var manFiles []string
err := filepath.Walk(realPath, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && matchNamePattern(info.Name(), namePattern) {
relPath, relErr := makeRelativePath(hc.Dir, p)
if relErr != nil {
return relErr
}
manFiles = append(manFiles, relPath)
}
return nil
})
if err != nil {
return fmt.Errorf("files-find-man: %w", err)
}
return outputFiles(hc, manFiles)
}
func filesFindConfigCmd(hc interp.HandlerContext, cmd string, args []string) error {
namePattern := "*"
if len(args) > 0 {
namePattern = args[0]
}
configPath := "./etc/"
realPath := path.Join(hc.Dir, configPath)
if err := validateDir(realPath, "files-find-config"); err != nil {
return err
}
var configFiles []string
err := filepath.Walk(realPath, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && matchNamePattern(info.Name(), namePattern) {
relPath, relErr := makeRelativePath(hc.Dir, p)
if relErr != nil {
return relErr
}
configFiles = append(configFiles, relPath)
}
return nil
})
if err != nil {
return fmt.Errorf("files-find-config: %w", err)
}
return outputFiles(hc, configFiles)
}

View File

@@ -56,18 +56,30 @@ var Helpers = handlers.ExecFuncs{
"install-library": installLibraryCmd, "install-library": installLibraryCmd,
"git-version": gitVersionCmd, "git-version": gitVersionCmd,
"files-find": filesFindCmd, "files-find": filesFindCmd,
"files-find-lang": filesFindLangCmd, "files-find-lang": filesFindLangCmd,
"files-find-doc": filesFindDocCmd, "files-find-doc": filesFindDocCmd,
"files-find-bin": filesFindBinCmd,
"files-find-lib": filesFindLibCmd,
"files-find-include": filesFindIncludeCmd,
"files-find-share": filesFindShareCmd,
"files-find-man": filesFindManCmd,
"files-find-config": filesFindConfigCmd,
} }
// Restricted contains restricted read-only helper commands // Restricted contains restricted read-only helper commands
// that don't modify any state // that don't modify any state
var Restricted = handlers.ExecFuncs{ var Restricted = handlers.ExecFuncs{
"git-version": gitVersionCmd, "git-version": gitVersionCmd,
"files-find": filesFindCmd, "files-find": filesFindCmd,
"files-find-lang": filesFindLangCmd, "files-find-lang": filesFindLangCmd,
"files-find-doc": filesFindDocCmd, "files-find-doc": filesFindDocCmd,
"files-find-bin": filesFindBinCmd,
"files-find-lib": filesFindLibCmd,
"files-find-include": filesFindIncludeCmd,
"files-find-share": filesFindShareCmd,
"files-find-man": filesFindManCmd,
"files-find-config": filesFindConfigCmd,
} }
func installHelperCmd(prefix string, perms os.FileMode) handlers.ExecFunc { func installHelperCmd(prefix string, perms os.FileMode) handlers.ExecFunc {

View File

@@ -19,7 +19,6 @@ package utils
import ( import (
"os" "os"
"os/exec" "os/exec"
"os/user"
"github.com/leonelquinteros/gotext" "github.com/leonelquinteros/gotext"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@@ -33,40 +32,10 @@ func IsNotRoot() bool {
return os.Getuid() != 0 return os.Getuid() != 0
} }
// EnuseIsPrivilegedGroupMember проверяет, что пользователь является членом привилегированной группы (wheel) // EnuseIsPrivilegedGroupMember проверяет, что пользователь является членом привилегированной группы (wheel/sudo)
// DEPRECATED: используйте CheckUserPrivileges() из utils.go
func EnuseIsPrivilegedGroupMember() error { func EnuseIsPrivilegedGroupMember() error {
// В CI пропускаем проверку группы wheel return CheckUserPrivileges()
if os.Getenv("CI") == "true" {
return nil
}
// Если пользователь root, пропускаем проверку
if os.Geteuid() == 0 {
return nil
}
currentUser, err := user.Current()
if err != nil {
return err
}
privilegedGroup := GetPrivilegedGroup()
group, err := user.LookupGroup(privilegedGroup)
if err != nil {
return err
}
groups, err := currentUser.GroupIds()
if err != nil {
return err
}
for _, gid := range groups {
if gid == group.Gid {
return nil
}
}
return cliutils.FormatCliExit(gotext.Get("You need to be a %s member to perform this action", privilegedGroup), nil)
} }
func RootNeededAction(f cli.ActionFunc) cli.ActionFunc { func RootNeededAction(f cli.ActionFunc) cli.ActionFunc {

View File

@@ -17,8 +17,10 @@
package utils package utils
import ( import (
"fmt"
"os" "os"
"os/exec" "os/exec"
"os/user"
"strings" "strings"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
@@ -28,66 +30,135 @@ 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 с правами для привилегированной группы // EnsureTempDirWithRootOwner создает каталог в /tmp/alr или /var/cache/alr с правами для привилегированной группы
// Все каталоги в /tmp/alr принадлежат root:привилегированная_группа с правами 775 // Все каталоги в /tmp/alr и /var/cache/alr принадлежат root:привилегированная_группа с правами 2775
// Для других каталогов использует стандартные права // Для других каталогов использует стандартные права
func EnsureTempDirWithRootOwner(path string, mode os.FileMode) error { func EnsureTempDirWithRootOwner(path string, mode os.FileMode) error {
if strings.HasPrefix(path, "/tmp/alr") { needsElevation := strings.HasPrefix(path, "/tmp/alr") || strings.HasPrefix(path, "/var/cache/alr")
// Сначала создаем директорию обычным способом
err := os.MkdirAll(path, mode) if needsElevation {
if err != nil {
return err
}
// В CI или если мы уже root, не нужно использовать sudo // В CI или если мы уже root, не нужно использовать sudo
isRoot := os.Geteuid() == 0 isRoot := os.Geteuid() == 0
isCI := os.Getenv("CI") == "true" isCI := os.Getenv("CI") == "true"
// В CI создаем директории с обычными правами // В CI создаем директории с обычными правами
if isCI { if isCI {
err := os.MkdirAll(path, mode)
if err != nil {
return err
}
// В CI не используем группу wheel и не меняем права // В CI не используем группу wheel и не меняем права
// Устанавливаем базовые права 777 для временных каталогов // Устанавливаем базовые права 777 для временных каталогов
chmodCmd := exec.Command("chmod", "777", path) chmodCmd := exec.Command("chmod", "777", path)
chmodCmd.Run() // Игнорируем ошибки chmodCmd.Run() // Игнорируем ошибки
return nil return nil
} }
// Для обычной работы устанавливаем права и привилегированную группу // Для обычной работы устанавливаем права и привилегированную группу
permissions := "2775" permissions := "2775"
group := GetPrivilegedGroup() group := GetPrivilegedGroup()
var chmodCmd, chownCmd *exec.Cmd var mkdirCmd, chmodCmd, chownCmd *exec.Cmd
if isRoot { if isRoot {
// Выполняем команды напрямую без sudo // Выполняем команды напрямую без sudo
mkdirCmd = exec.Command("mkdir", "-p", path)
chmodCmd = exec.Command("chmod", permissions, path) chmodCmd = exec.Command("chmod", permissions, path)
chownCmd = exec.Command("chown", "root:"+group, path) chownCmd = exec.Command("chown", "root:"+group, path)
} else { } else {
// Используем sudo для обычных пользователей // Используем sudo для всех операций с привилегированными каталогами
mkdirCmd = exec.Command("sudo", "mkdir", "-p", path)
chmodCmd = exec.Command("sudo", "chmod", permissions, path) chmodCmd = exec.Command("sudo", "chmod", permissions, path)
chownCmd = exec.Command("sudo", "chown", "root:"+group, path) chownCmd = exec.Command("sudo", "chown", "root:"+group, path)
} }
// Устанавливаем права с setgid битом // Создаем директорию через sudo если нужно
err := mkdirCmd.Run()
if err != nil {
// Игнорируем ошибку если директория уже существует
if !isRoot {
// Проверяем существует ли директория
if _, statErr := os.Stat(path); statErr != nil {
return fmt.Errorf("не удалось создать директорию %s: %w", path, err)
}
}
}
// Устанавливаем права с setgid битом для наследования группы
err = chmodCmd.Run() err = chmodCmd.Run()
if err != nil { if err != nil {
// Для root игнорируем ошибки, если группа не существует
if !isRoot { if !isRoot {
return err return fmt.Errorf("не удалось установить права на %s: %w", path, err)
} }
} }
// Устанавливаем владельца root:wheel // Устанавливаем владельца root:группа
err = chownCmd.Run() err = chownCmd.Run()
if err != nil { if err != nil {
// Для root игнорируем ошибки, если группа не существует
if !isRoot { if !isRoot {
return err return fmt.Errorf("не удалось установить владельца на %s: %w", path, err)
} }
} }
return nil return nil
} }
// Для остальных каталогов обычное создание // Для остальных каталогов обычное создание
return os.MkdirAll(path, mode) return os.MkdirAll(path, mode)
} }
// IsUserInGroup проверяет, состоит ли пользователь в указанной группе
func IsUserInGroup(username, groupname string) bool {
u, err := user.Lookup(username)
if err != nil {
return false
}
groups, err := u.GroupIds()
if err != nil {
return false
}
targetGroup, err := user.LookupGroup(groupname)
if err != nil {
return false
}
for _, gid := range groups {
if gid == targetGroup.Gid {
return true
}
}
return false
}
// CheckUserPrivileges проверяет, что пользователь имеет необходимые привилегии для работы с ALR
// Пользователь должен быть root или состоять в группе wheel/sudo
func CheckUserPrivileges() error {
// Если пользователь root - все в порядке
if os.Geteuid() == 0 {
return nil
}
// В CI не проверяем привилегии
if os.Getenv("CI") == "true" {
return nil
}
currentUser, err := user.Current()
if err != nil {
return fmt.Errorf("не удалось получить информацию о текущем пользователе: %w", err)
}
privilegedGroup := GetPrivilegedGroup()
// Проверяем членство в привилегированной группе
if !IsUserInGroup(currentUser.Username, privilegedGroup) {
return fmt.Errorf("пользователь %s не имеет необходимых привилегий для работы с ALR.\n"+
"Для работы с ALR необходимо быть пользователем root или состоять в группе %s.\n"+
"Выполните команду: sudo usermod -a -G %s %s\n"+
"Затем перезайдите в систему или выполните: newgrp %s",
currentUser.Username, privilegedGroup, privilegedGroup, currentUser.Username, privilegedGroup)
}
return nil
}

View File

@@ -21,6 +21,7 @@ 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 {
@@ -29,6 +30,9 @@ 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.CheckUserPrivileges(); err != nil {
return err
}
ctx := c.Context ctx := c.Context