diff --git a/README.md b/README.md index 0c90d427..270af7ce 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ The configuration file is a JSON file with the following structure: "iPkgMngAdd": "apt install -y", "iPkgMngRm": "apt remove -y", "iPkgMngApi": "https://packages.vanillaos.org/api/pkg/{packageName}", + "IPkgMngStatus": 0, "differURL": "https://differ.vanillaos.org", @@ -100,6 +101,7 @@ The following table describes each of the configuration options: | `iPkgMngAdd` | The command to run when adding a package. It can be a command or a script. | | `iPkgMngRm` | The command to run when removing a package. It can be a command or a script. | | `iPkgMngApi` | The API endpoint to use when querying for package information. If not set, ABRoot will not check if a package exists before installing it. This could lead to errors. Take a look at our [Eratosthenes API](https://github.com/Vanilla-OS/Eratosthenes/blob/388e6f724dcda94ee60964e7b12a78ad79fb8a40/eratosthenes.py#L52) for an example. | +| `IPkgMngStatus` | The status of the package manager feature. The value '0' means that the feature is disabled, the value '1' means enabled and the value '2' means that it will require user agreement the first time it is used. If the feature is disabled, it will not appear in the commands list. | | `differURL` | The URL of the [Differ API](https://github.com/Vanilla-OS/Differ) service to use when comparing two OCI images. | | `partLabelVar` | The label of the partition dedicated to the system's `/var` directory. | | `partLabelA` | The label of the partition dedicated to the system's `A` root. | diff --git a/cmd/pkg.go b/cmd/pkg.go index a67ec130..be75f5e7 100644 --- a/cmd/pkg.go +++ b/cmd/pkg.go @@ -14,7 +14,9 @@ package cmd */ import ( + "bufio" "errors" + "os" "strings" "github.com/spf13/cobra" @@ -40,6 +42,13 @@ func NewPkgCommand() *cmdr.Command { abroot.Trans("pkg.dryRunFlag"), false)) + cmd.WithBoolFlag( + cmdr.NewBoolFlag( + "force-enable-user-agreement", + "f", + abroot.Trans("pkg.forceEnableUserAgreementFlag"), + false)) + cmd.Args = cobra.MinimumNArgs(1) cmd.ValidArgs = validPkgArgs cmd.Example = "abroot pkg add " @@ -59,8 +68,48 @@ func pkg(cmd *cobra.Command, args []string) error { return err } + forceEnableUserAgreement, err := cmd.Flags().GetBool("force-enable-user-agreement") + if err != nil { + cmdr.Error.Println(err) + return err + } + pkgM := core.NewPackageManager(false) + // Check for user agreement, here we could simply call the CheckStatus + // function which also checks if the package manager is enabled or not + // since this pkg command is not even added to the root command if the + // package manager is disabled, but we want to be explicit here to avoid + // potential hard to debug errors in the future in weird development + // scenarios. Yeah, trust me, I've been there. + if pkgM.Status == core.PKG_MNG_REQ_AGREEMENT { + err = pkgM.CheckStatus() + if err != nil { + if !forceEnableUserAgreement { + cmdr.Info.Println(abroot.Trans("pkg.agreementMsg")) + reader := bufio.NewReader(os.Stdin) + answer, _ := reader.ReadString('\n') + answer = strings.TrimSpace(answer) + if answer == "y" || answer == "Y" { + err := pkgM.AcceptUserAgreement() + if err != nil { + cmdr.Error.Println(abroot.Trans("pkg.agreementSignFailed"), err) + return err + } + } else { + cmdr.Info.Println(abroot.Trans("pkg.agreementDeclined")) + return nil + } + } else { + err := pkgM.AcceptUserAgreement() + if err != nil { + cmdr.Error.Println(abroot.Trans("pkg.agreementSignFailed"), err) + return err + } + } + } + } + switch args[0] { case "add": if len(args) < 2 { diff --git a/cmd/status.go b/cmd/status.go index 19fb81d8..425efa8c 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -96,7 +96,12 @@ func status(cmd *cobra.Command, args []string) error { return err } + pkgMngAgreementStatus := false pkgMng := core.NewPackageManager(false) + if pkgMng.Status == core.PKG_MNG_REQ_AGREEMENT { + err = pkgMng.CheckStatus() + pkgMngAgreementStatus = err == nil + } pkgsAdd, err := pkgMng.GetAddPackages() if err != nil { return err @@ -112,31 +117,35 @@ func status(cmd *cobra.Command, args []string) error { if jsonFlag || dumpFlag { type status struct { - Present string `json:"present"` - Future string `json:"future"` - CnfFile string `json:"cnfFile"` - CPU string `json:"cpu"` - GPU []string `json:"gpu"` - Memory string `json:"memory"` - ABImage core.ABImage `json:"abimage"` - Kargs string `json:"kargs"` - PkgsAdd []string `json:"pkgsAdd"` - PkgsRm []string `json:"pkgsRm"` - PkgsUnstg []string `json:"pkgsUnstg"` + Present string `json:"present"` + Future string `json:"future"` + CnfFile string `json:"cnfFile"` + CPU string `json:"cpu"` + GPU []string `json:"gpu"` + Memory string `json:"memory"` + ABImage core.ABImage `json:"abimage"` + Kargs string `json:"kargs"` + PkgsAdd []string `json:"pkgsAdd"` + PkgsRm []string `json:"pkgsRm"` + PkgsUnstg []string `json:"pkgsUnstg"` + PkgMngStatus int `json:"pkgMngStatus"` + PkgMngAgreement bool `json:"pkgMngAg"` } s := status{ - Present: present.Label, - Future: future.Label, - CnfFile: settings.CnfFileUsed, - CPU: specs.CPU, - GPU: specs.GPU, - Memory: specs.Memory, - ABImage: *abImage, - Kargs: kargs, - PkgsAdd: pkgsAdd, - PkgsRm: pkgsRm, - PkgsUnstg: pkgsUnstg, + Present: present.Label, + Future: future.Label, + CnfFile: settings.CnfFileUsed, + CPU: specs.CPU, + GPU: specs.GPU, + Memory: specs.Memory, + ABImage: *abImage, + Kargs: kargs, + PkgsAdd: pkgsAdd, + PkgsRm: pkgsRm, + PkgsUnstg: pkgsUnstg, + PkgMngStatus: settings.Cnf.IPkgMngStatus, + PkgMngAgreement: pkgMngAgreementStatus, } b, err := json.Marshal(s) @@ -221,15 +230,22 @@ func status(cmd *cobra.Command, args []string) error { unstagedAlert = fmt.Sprintf(abroot.Trans("status.unstagedFoundMsg"), len(pkgsUnstg)) } - cmdr.Info.Printf( - abroot.Trans("status.infoMsg"), - present.Label, future.Label, - settings.CnfFileUsed, - specs.CPU, formattedGPU, specs.Memory, - abImage.Digest, abImage.Timestamp.Format("2006-01-02 15:04:05"), abImage.Image, - kargs, - strings.Join(pkgsAdd, ", "), strings.Join(pkgsRm, ", "), strings.Join(pkgsUnstg, ", "), - unstagedAlert, + agreementMsg := "" + if settings.Cnf.IPkgMngStatus == 2 { + agreementMsg = fmt.Sprintf(abroot.Trans("status.infoMsgAgreementStatus"), pkgMngAgreementStatus) + } + cmdr.Info.Printfln("%s, %s", + fmt.Sprintf( + abroot.Trans("status.infoMsg"), + present.Label, future.Label, + settings.CnfFileUsed, + specs.CPU, formattedGPU, specs.Memory, + abImage.Digest, abImage.Timestamp.Format("2006-01-02 15:04:05"), abImage.Image, + kargs, + strings.Join(pkgsAdd, ", "), strings.Join(pkgsRm, ", "), strings.Join(pkgsUnstg, ", "), + unstagedAlert, + ), + agreementMsg, ) return nil diff --git a/core/packages.go b/core/packages.go index ee520022..6a416d22 100644 --- a/core/packages.go +++ b/core/packages.go @@ -23,6 +23,7 @@ import ( "os" "path/filepath" "strings" + "time" "github.com/vanilla-os/abroot/settings" ) @@ -31,21 +32,36 @@ import ( type PackageManager struct { dryRun bool baseDir string + Status ABRootPkgManagerStatus } +// Common Package manager paths const ( - PackagesBaseDir = "/etc/abroot" - DryRunPackagesBaseDir = "/tmp/abroot" - PackagesAddFile = "packages.add" - PackagesRemoveFile = "packages.remove" - PackagesUnstagedFile = "packages.unstaged" + PackagesBaseDir = "/etc/abroot" + PkgManagerUserAgreementFile = "/etc/abroot/ABPkgManager.userAgreement" + DryRunPackagesBaseDir = "/tmp/abroot" + PackagesAddFile = "packages.add" + PackagesRemoveFile = "packages.remove" + PackagesUnstagedFile = "packages.unstaged" ) +// Package manager operations const ( ADD = "+" REMOVE = "-" ) +// Package manager statuses +const ( + PKG_MNG_DISABLED = 0 + PKG_MNG_ENABLED = 1 + PKG_MNG_REQ_AGREEMENT = 2 +) + +// ABRootPkgManagerStatus represents the status of the package manager +// in the ABRoot configuration file +type ABRootPkgManagerStatus int + // An unstaged package is a package that is waiting to be applied // to the next root. // @@ -110,13 +126,32 @@ func NewPackageManager(dryRun bool) *PackageManager { } } - return &PackageManager{dryRun, baseDir} + // here we convert settings.Cnf.IPkgMngStatus to an ABRootPkgManagerStatus + // for easier understanding in the code + var status ABRootPkgManagerStatus + switch settings.Cnf.IPkgMngStatus { + case PKG_MNG_REQ_AGREEMENT: + status = PKG_MNG_REQ_AGREEMENT + case PKG_MNG_ENABLED: + status = PKG_MNG_ENABLED + default: + status = PKG_MNG_DISABLED + } + + return &PackageManager{dryRun, baseDir, status} } // Add adds a package to the packages.add file func (p *PackageManager) Add(pkg string) error { PrintVerboseInfo("PackageManager.Add", "running...") + // Check for package manager status and user agreement + err := p.CheckStatus() + if err != nil { + PrintVerboseErr("PackageManager.Add", 0, err) + return err + } + // Check if package was removed before packageWasRemoved := false removedIndex := -1 @@ -189,6 +224,13 @@ func (p *PackageManager) Add(pkg string) error { func (p *PackageManager) Remove(pkg string) error { PrintVerboseInfo("PackageManager.Remove", "running...") + // Check for package manager status and user agreement + err := p.CheckStatus() + if err != nil { + PrintVerboseErr("PackageManager.Add", 0, err) + return err + } + // Add to unstaged packages first upkgs, err := p.GetUnstagedPackages() if err != nil { @@ -599,3 +641,68 @@ func GetRepoContentsForPkg(pkg string) (map[string]interface{}, error) { return pkgInfo, nil } + +// AcceptUserAgreement sets the package manager status to enabled +func (p *PackageManager) AcceptUserAgreement() error { + PrintVerboseInfo("PackageManager.AcceptUserAgreement", "running...") + + if p.Status != PKG_MNG_REQ_AGREEMENT { + PrintVerboseInfo("PackageManager.AcceptUserAgreement", "package manager is not in agreement mode") + return nil + } + + err := os.WriteFile( + PkgManagerUserAgreementFile, + []byte(time.Now().String()), + 0644, + ) + if err != nil { + PrintVerboseErr("PackageManager.AcceptUserAgreement", 0, err) + return err + } + + return nil +} + +// GetUserAgreementStatus returns if the user has accepted the package manager +// agreement or not +func (p *PackageManager) GetUserAgreementStatus() bool { + PrintVerboseInfo("PackageManager.GetUserAgreementStatus", "running...") + + if p.Status != PKG_MNG_REQ_AGREEMENT { + PrintVerboseInfo("PackageManager.GetUserAgreementStatus", "package manager is not in agreement mode") + return true + } + + _, err := os.Stat(PkgManagerUserAgreementFile) + if err != nil { + PrintVerboseInfo("PackageManager.GetUserAgreementStatus", "user has not accepted the agreement") + return false + } + + PrintVerboseInfo("PackageManager.GetUserAgreementStatus", "user has accepted the agreement") + return true +} + +// CheckStatus checks if the package manager is enabled or not +func (p *PackageManager) CheckStatus() error { + PrintVerboseInfo("PackageManager.CheckStatus", "running...") + + // Check if package manager is enabled + if p.Status == PKG_MNG_DISABLED { + PrintVerboseInfo("PackageManager.CheckStatus", "package manager is disabled") + return nil + } + + // Check if user has accepted the package manager agreement + if p.Status == PKG_MNG_REQ_AGREEMENT { + if !p.GetUserAgreementStatus() { + PrintVerboseInfo("PackageManager.CheckStatus", "package manager agreement not accepted") + err := errors.New("package manager agreement not accepted") + return err + } + } + + PrintVerboseInfo("PackageManager.CheckStatus", "package manager is enabled") + return nil +} diff --git a/locales/en.yml b/locales/en.yml index 97d14d80..9bcc7dd2 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -36,6 +36,10 @@ pkg: removedMsg: "Package(s) %s removed.\n" listMsg: "Added packages:\n%s\nRemoved packages:\n%s\n" dryRunFlag: "perform a dry run of the operation" + forceEnableUserAgreementFlag: "force enable user agreement, for embedded systems" + agreementMsg: "To utilize ABRoot's abroot pkg command, explicit user agreement is required. This command facilitates package installations but introduces non-deterministic elements, impacting system trustworthiness. By consenting, you acknowledge and accept these implications, confirming your awareness of the command's potential impact on system behavior. [y/N]: " + agreementSignFailed: "Failed to sign the agreement: %s\n" + agreementDeclined: "You declined the agreement. The feature will stay disabled until you agree to it." status: use: "status" @@ -67,6 +71,7 @@ status: - Added: %s - Removed: %s - Unstaged: %s%s + infoMsgAgreementStatus: "\nPackage agreement: %t" unstagedFoundMsg: "\n\t\tThere are %d unstaged packages. Please run 'abroot pkg apply' to apply them." dumpMsg: "Dumped ABRoot status to %s\n" diff --git a/main.go b/main.go index d85b4536..d46b09bd 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ import ( "github.com/containers/storage/pkg/reexec" "github.com/vanilla-os/abroot/cmd" + "github.com/vanilla-os/abroot/settings" "github.com/vanilla-os/orchid/cmdr" ) @@ -46,8 +47,12 @@ func main() { kargs := cmd.NewKargsCommand() root.AddCommand(kargs) - pkg := cmd.NewPkgCommand() - root.AddCommand(pkg) + // we only add the pkg command if the ABRoot configuration + // has the IPkgMng enabled in any way (1 or 2) + if settings.Cnf.IPkgMngStatus > 0 { + pkg := cmd.NewPkgCommand() + root.AddCommand(pkg) + } rollback := cmd.NewRollbackCommand() root.AddCommand(rollback) diff --git a/settings/config.go b/settings/config.go index 4c2fe8b0..ca4818ad 100644 --- a/settings/config.go +++ b/settings/config.go @@ -33,11 +33,12 @@ type Config struct { Tag string `json:"tag"` // Package manager - IPkgMngPre string `json:"iPkgMngPre"` - IPkgMngPost string `json:"iPkgMngPost"` - IPkgMngAdd string `json:"iPkgMngAdd"` - IPkgMngRm string `json:"iPkgMngRm"` - IPkgMngApi string `json:"iPkgMngApi"` + IPkgMngPre string `json:"iPkgMngPre"` + IPkgMngPost string `json:"iPkgMngPost"` + IPkgMngAdd string `json:"iPkgMngAdd"` + IPkgMngRm string `json:"iPkgMngRm"` + IPkgMngApi string `json:"iPkgMngApi"` + IPkgMngStatus int `json:"iPkgMngStatus"` // Package diff API (Differ) DifferURL string `json:"differURL"` @@ -99,11 +100,12 @@ func init() { Tag: viper.GetString("tag"), // Package manager - IPkgMngPre: viper.GetString("iPkgMngPre"), - IPkgMngPost: viper.GetString("iPkgMngPost"), - IPkgMngAdd: viper.GetString("iPkgMngAdd"), - IPkgMngRm: viper.GetString("iPkgMngRm"), - IPkgMngApi: viper.GetString("iPkgMngApi"), + IPkgMngPre: viper.GetString("iPkgMngPre"), + IPkgMngPost: viper.GetString("iPkgMngPost"), + IPkgMngAdd: viper.GetString("iPkgMngAdd"), + IPkgMngRm: viper.GetString("iPkgMngRm"), + IPkgMngApi: viper.GetString("iPkgMngApi"), + IPkgMngStatus: viper.GetInt("iPkgMngStatus"), // Package diff API (Differ) DifferURL: viper.GetString("differURL"),