Skip to content

Commit

Permalink
feat[close #189]: Ask the user consent to unlock the pkg manager feature
Browse files Browse the repository at this point in the history
  • Loading branch information
mirkobrombin committed Jan 9, 2024
1 parent 0432c0c commit 997fa36
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 49 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",

Expand Down Expand Up @@ -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. |
Expand Down
49 changes: 49 additions & 0 deletions cmd/pkg.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ package cmd
*/

import (
"bufio"
"errors"
"os"
"strings"

"github.com/spf13/cobra"
Expand All @@ -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 <pkg>"
Expand All @@ -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 {
Expand Down
78 changes: 47 additions & 31 deletions cmd/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
119 changes: 113 additions & 6 deletions core/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"os"
"path/filepath"
"strings"
"time"

"github.com/vanilla-os/abroot/settings"
)
Expand All @@ -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.
//
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
5 changes: 5 additions & 0 deletions locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
Loading

0 comments on commit 997fa36

Please sign in to comment.