Compare commits
10 Commits
064db9f020
...
cdda33555c
Author | SHA1 | Date |
---|---|---|
Ophestra Umiker | cdda33555c | |
Ophestra Umiker | ad0034b09a | |
Ophestra Umiker | 1da845d78b | |
Ophestra Umiker | 55bb348d5f | |
Ophestra Umiker | ecce832d93 | |
Ophestra Umiker | 65bd7d18db | |
Ophestra Umiker | 4ebb98649e | |
Ophestra Umiker | 919e5b5cd5 | |
Ophestra Umiker | 40161c5938 | |
Ophestra Umiker | 679e719f9e |
|
@ -21,8 +21,8 @@ jobs:
|
||||||
|
|
||||||
- name: Get dependencies
|
- name: Get dependencies
|
||||||
run: >-
|
run: >-
|
||||||
sudo apt-get update &&
|
apt-get update &&
|
||||||
sudo apt-get install -y
|
apt-get install -y
|
||||||
gcc
|
gcc
|
||||||
pkg-config
|
pkg-config
|
||||||
libacl1-dev
|
libacl1-dev
|
||||||
|
|
|
@ -20,8 +20,8 @@ jobs:
|
||||||
|
|
||||||
- name: Get dependencies
|
- name: Get dependencies
|
||||||
run: >-
|
run: >-
|
||||||
sudo apt-get update &&
|
apt-get update &&
|
||||||
sudo apt-get install -y
|
apt-get install -y
|
||||||
gcc
|
gcc
|
||||||
pkg-config
|
pkg-config
|
||||||
libacl1-dev
|
libacl1-dev
|
||||||
|
|
|
@ -3,7 +3,7 @@ Fortify
|
||||||
|
|
||||||
[![Go Reference](https://pkg.go.dev/badge/git.ophivana.moe/cat/fortify.svg)](https://pkg.go.dev/git.ophivana.moe/cat/fortify)
|
[![Go Reference](https://pkg.go.dev/badge/git.ophivana.moe/cat/fortify.svg)](https://pkg.go.dev/git.ophivana.moe/cat/fortify)
|
||||||
|
|
||||||
Lets you run graphical applications as another user ~~in an Android-like sandbox environment~~ (WIP) with a nice NixOS
|
Lets you run graphical applications as another user in a confined environment with a nice NixOS
|
||||||
module to configure target users and provide launchers and desktop files for your privileged user.
|
module to configure target users and provide launchers and desktop files for your privileged user.
|
||||||
|
|
||||||
Why would you want this?
|
Why would you want this?
|
||||||
|
@ -12,7 +12,7 @@ Why would you want this?
|
||||||
|
|
||||||
- It protects applications from each other.
|
- It protects applications from each other.
|
||||||
|
|
||||||
- It provides UID isolation on top of ~~the standard application sandbox~~ (WIP).
|
- It provides UID isolation on top of the standard application sandbox.
|
||||||
|
|
||||||
There are a few different things to set up for this to work:
|
There are a few different things to set up for this to work:
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,12 @@
|
||||||
|
|
||||||
devShells = forAllSystems (system: {
|
devShells = forAllSystems (system: {
|
||||||
default = nixpkgsFor.${system}.mkShell {
|
default = nixpkgsFor.${system}.mkShell {
|
||||||
|
buildInputs =
|
||||||
|
with nixpkgsFor.${system};
|
||||||
|
self.packages.${system}.fortify.buildInputs;
|
||||||
|
};
|
||||||
|
|
||||||
|
withPackage = nixpkgsFor.${system}.mkShell {
|
||||||
buildInputs =
|
buildInputs =
|
||||||
with nixpkgsFor.${system};
|
with nixpkgsFor.${system};
|
||||||
self.packages.${system}.fortify.buildInputs ++ [ self.packages.${system}.fortify ];
|
self.packages.${system}.fortify.buildInputs ++ [ self.packages.${system}.fortify ];
|
||||||
|
|
|
@ -7,26 +7,39 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type App interface {
|
type App interface {
|
||||||
Seal(config *Config) error
|
// ID returns a copy of App's unique ID.
|
||||||
|
ID() ID
|
||||||
|
// Start sets up the system and starts the App.
|
||||||
Start() error
|
Start() error
|
||||||
|
// Wait waits for App's process to exit and reverts system setup.
|
||||||
Wait() (int, error)
|
Wait() (int, error)
|
||||||
|
// WaitErr returns error returned by the underlying wait syscall.
|
||||||
WaitErr() error
|
WaitErr() error
|
||||||
|
|
||||||
|
Seal(config *Config) error
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
type app struct {
|
type app struct {
|
||||||
|
// application unique identifier
|
||||||
|
id *ID
|
||||||
|
// underlying user switcher process
|
||||||
|
cmd *exec.Cmd
|
||||||
// child process related information
|
// child process related information
|
||||||
seal *appSeal
|
seal *appSeal
|
||||||
// underlying fortified child process
|
|
||||||
cmd *exec.Cmd
|
|
||||||
// wayland connection if wayland mediation is enabled
|
// wayland connection if wayland mediation is enabled
|
||||||
wayland *net.UnixConn
|
wayland *net.UnixConn
|
||||||
// error returned waiting for process
|
// error returned waiting for process
|
||||||
wait error
|
waitErr error
|
||||||
|
|
||||||
lock sync.RWMutex
|
lock sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *app) ID() ID {
|
||||||
|
return *a.id
|
||||||
|
}
|
||||||
|
|
||||||
func (a *app) String() string {
|
func (a *app) String() string {
|
||||||
if a == nil {
|
if a == nil {
|
||||||
return "(invalid fortified app)"
|
return "(invalid fortified app)"
|
||||||
|
@ -47,9 +60,11 @@ func (a *app) String() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *app) WaitErr() error {
|
func (a *app) WaitErr() error {
|
||||||
return a.wait
|
return a.waitErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() App {
|
func New() (App, error) {
|
||||||
return new(app)
|
a := new(app)
|
||||||
|
a.id = new(ID)
|
||||||
|
return a, newAppID(a.id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,14 +5,13 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
)
|
)
|
||||||
|
|
||||||
type appID [16]byte
|
type ID [16]byte
|
||||||
|
|
||||||
func (a *appID) String() string {
|
func (a *ID) String() string {
|
||||||
return hex.EncodeToString(a[:])
|
return hex.EncodeToString(a[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAppID() (*appID, error) {
|
func newAppID(id *ID) error {
|
||||||
a := &appID{}
|
_, err := rand.Read(id[:])
|
||||||
_, err := rand.Read(a[:])
|
return err
|
||||||
return a, err
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,11 @@ const (
|
||||||
LaunchMethodMachineCtl
|
LaunchMethodMachineCtl
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var method = [...]string{
|
||||||
|
LaunchMethodSudo: "sudo",
|
||||||
|
LaunchMethodMachineCtl: "systemd",
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrConfig = errors.New("no configuration to seal")
|
ErrConfig = errors.New("no configuration to seal")
|
||||||
ErrUser = errors.New("unknown user")
|
ErrUser = errors.New("unknown user")
|
||||||
|
@ -48,24 +53,17 @@ func (a *app) Seal(config *Config) error {
|
||||||
// create seal
|
// create seal
|
||||||
seal := new(appSeal)
|
seal := new(appSeal)
|
||||||
|
|
||||||
// generate application ID
|
|
||||||
if id, err := newAppID(); err != nil {
|
|
||||||
return fmsg.WrapErrorSuffix(err,
|
|
||||||
"cannot generate application ID:")
|
|
||||||
} else {
|
|
||||||
seal.id = id
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch system constants
|
// fetch system constants
|
||||||
seal.SystemConstants = internal.GetSC()
|
seal.SystemConstants = internal.GetSC()
|
||||||
|
|
||||||
// pass through config values
|
// pass through config values
|
||||||
|
seal.id = a.id.String()
|
||||||
seal.fid = config.ID
|
seal.fid = config.ID
|
||||||
seal.command = config.Command
|
seal.command = config.Command
|
||||||
|
|
||||||
// parses launch method text and looks up tool path
|
// parses launch method text and looks up tool path
|
||||||
switch config.Method {
|
switch config.Method {
|
||||||
case "sudo":
|
case method[LaunchMethodSudo]:
|
||||||
seal.launchOption = LaunchMethodSudo
|
seal.launchOption = LaunchMethodSudo
|
||||||
if sudoPath, err := exec.LookPath("sudo"); err != nil {
|
if sudoPath, err := exec.LookPath("sudo"); err != nil {
|
||||||
return fmsg.WrapError(ErrSudo,
|
return fmsg.WrapError(ErrSudo,
|
||||||
|
@ -73,7 +71,7 @@ func (a *app) Seal(config *Config) error {
|
||||||
} else {
|
} else {
|
||||||
seal.toolPath = sudoPath
|
seal.toolPath = sudoPath
|
||||||
}
|
}
|
||||||
case "systemd":
|
case method[LaunchMethodMachineCtl]:
|
||||||
seal.launchOption = LaunchMethodMachineCtl
|
seal.launchOption = LaunchMethodMachineCtl
|
||||||
if !internal.SdBootedV {
|
if !internal.SdBootedV {
|
||||||
return fmsg.WrapError(ErrSystemd,
|
return fmsg.WrapError(ErrSystemd,
|
||||||
|
|
|
@ -31,17 +31,8 @@ func (seal *appSeal) shareRuntime() {
|
||||||
// ensure runtime directory ACL (e.g. `/run/user/%d`)
|
// ensure runtime directory ACL (e.g. `/run/user/%d`)
|
||||||
seal.sys.UpdatePermType(system.User, seal.RuntimePath, acl.Execute)
|
seal.sys.UpdatePermType(system.User, seal.RuntimePath, acl.Execute)
|
||||||
|
|
||||||
// ensure Share (e.g. `/tmp/fortify.%d`)
|
|
||||||
// acl is unnecessary as this directory is world executable
|
|
||||||
seal.sys.Ensure(seal.SharePath, 0701)
|
|
||||||
|
|
||||||
// ensure process-specific share (e.g. `/tmp/fortify.%d/%s`)
|
|
||||||
// acl is unnecessary as this directory is world executable
|
|
||||||
seal.share = path.Join(seal.SharePath, seal.id.String())
|
|
||||||
seal.sys.Ephemeral(system.Process, seal.share, 0701)
|
|
||||||
|
|
||||||
// ensure process-specific share local to XDG_RUNTIME_DIR (e.g. `/run/user/%d/fortify/%s`)
|
// ensure process-specific share local to XDG_RUNTIME_DIR (e.g. `/run/user/%d/fortify/%s`)
|
||||||
seal.shareLocal = path.Join(seal.RunDirPath, seal.id.String())
|
seal.shareLocal = path.Join(seal.RunDirPath, seal.id)
|
||||||
seal.sys.Ephemeral(system.Process, seal.shareLocal, 0700)
|
seal.sys.Ephemeral(system.Process, seal.shareLocal, 0700)
|
||||||
seal.sys.UpdatePerm(seal.shareLocal, acl.Execute)
|
seal.sys.UpdatePerm(seal.shareLocal, acl.Execute)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,31 @@ const (
|
||||||
|
|
||||||
// shareSystem queues various system-related actions
|
// shareSystem queues various system-related actions
|
||||||
func (seal *appSeal) shareSystem() {
|
func (seal *appSeal) shareSystem() {
|
||||||
|
// ensure Share (e.g. `/tmp/fortify.%d`)
|
||||||
|
// acl is unnecessary as this directory is world executable
|
||||||
|
seal.sys.Ensure(seal.SharePath, 0701)
|
||||||
|
|
||||||
|
// ensure process-specific share (e.g. `/tmp/fortify.%d/%s`)
|
||||||
|
// acl is unnecessary as this directory is world executable
|
||||||
|
seal.share = path.Join(seal.SharePath, seal.id)
|
||||||
|
seal.sys.Ephemeral(system.Process, seal.share, 0701)
|
||||||
|
|
||||||
|
// ensure child tmpdir parent directory (e.g. `/tmp/fortify.%d/tmpdir`)
|
||||||
|
targetTmpdirParent := path.Join(seal.SharePath, "tmpdir")
|
||||||
|
seal.sys.Ensure(targetTmpdirParent, 0700)
|
||||||
|
seal.sys.UpdatePermType(system.User, targetTmpdirParent, acl.Execute)
|
||||||
|
|
||||||
|
// ensure child tmpdir (e.g. `/tmp/fortify.%d/tmpdir/%d`)
|
||||||
|
targetTmpdir := path.Join(targetTmpdirParent, seal.sys.user.Uid)
|
||||||
|
seal.sys.Ensure(targetTmpdir, 01700)
|
||||||
|
seal.sys.UpdatePermType(system.User, targetTmpdir, acl.Read, acl.Write, acl.Execute)
|
||||||
|
seal.sys.bwrap.Bind(targetTmpdir, "/tmp", false, true)
|
||||||
|
|
||||||
|
// mount tmpfs on inner shared directory (e.g. `/tmp/fortify.%d`)
|
||||||
|
seal.sys.bwrap.Tmpfs(seal.SharePath, 1*1024*1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (seal *appSeal) sharePasswd() {
|
||||||
// look up shell
|
// look up shell
|
||||||
sh := "/bin/sh"
|
sh := "/bin/sh"
|
||||||
if s, ok := os.LookupEnv(shell); ok {
|
if s, ok := os.LookupEnv(shell); ok {
|
||||||
|
@ -44,21 +69,3 @@ func (seal *appSeal) shareSystem() {
|
||||||
seal.sys.bwrap.Bind(passwdPath, "/etc/passwd")
|
seal.sys.bwrap.Bind(passwdPath, "/etc/passwd")
|
||||||
seal.sys.bwrap.Bind(groupPath, "/etc/group")
|
seal.sys.bwrap.Bind(groupPath, "/etc/group")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (seal *appSeal) shareTmpdirChild() string {
|
|
||||||
// ensure child tmpdir parent directory (e.g. `/tmp/fortify.%d/tmpdir`)
|
|
||||||
targetTmpdirParent := path.Join(seal.SharePath, "tmpdir")
|
|
||||||
seal.sys.Ensure(targetTmpdirParent, 0700)
|
|
||||||
seal.sys.UpdatePermType(system.User, targetTmpdirParent, acl.Execute)
|
|
||||||
|
|
||||||
// ensure child tmpdir (e.g. `/tmp/fortify.%d/tmpdir/%d`)
|
|
||||||
targetTmpdir := path.Join(targetTmpdirParent, seal.sys.user.Uid)
|
|
||||||
seal.sys.Ensure(targetTmpdir, 01700)
|
|
||||||
seal.sys.UpdatePermType(system.User, targetTmpdir, acl.Read, acl.Write, acl.Execute)
|
|
||||||
seal.sys.bwrap.Bind(targetTmpdir, "/tmp", false, true)
|
|
||||||
|
|
||||||
// mount tmpfs on inner shared directory (e.g. `/tmp/fortify.%d`)
|
|
||||||
seal.sys.bwrap.Tmpfs(seal.SharePath, 1*1024*1024)
|
|
||||||
|
|
||||||
return targetTmpdir
|
|
||||||
}
|
|
||||||
|
|
|
@ -94,7 +94,7 @@ func (a *app) Start() error {
|
||||||
PID: a.cmd.Process.Pid,
|
PID: a.cmd.Process.Pid,
|
||||||
Command: a.seal.command,
|
Command: a.seal.command,
|
||||||
Capability: a.seal.et,
|
Capability: a.seal.et,
|
||||||
Launcher: a.seal.toolPath,
|
Method: method[a.seal.launchOption],
|
||||||
Argv: a.cmd.Args,
|
Argv: a.cmd.Args,
|
||||||
Time: startTime,
|
Time: startTime,
|
||||||
}
|
}
|
||||||
|
@ -173,7 +173,7 @@ func (a *app) Wait() (int, error) {
|
||||||
var exitError *exec.ExitError
|
var exitError *exec.ExitError
|
||||||
if !errors.As(err, &exitError) {
|
if !errors.As(err, &exitError) {
|
||||||
// should be unreachable
|
// should be unreachable
|
||||||
a.wait = err
|
a.waitErr = err
|
||||||
}
|
}
|
||||||
|
|
||||||
// store non-zero return code
|
// store non-zero return code
|
||||||
|
|
|
@ -8,19 +8,18 @@ import (
|
||||||
"git.ophivana.moe/cat/fortify/internal"
|
"git.ophivana.moe/cat/fortify/internal"
|
||||||
"git.ophivana.moe/cat/fortify/internal/state"
|
"git.ophivana.moe/cat/fortify/internal/state"
|
||||||
"git.ophivana.moe/cat/fortify/internal/system"
|
"git.ophivana.moe/cat/fortify/internal/system"
|
||||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// appSeal seals the application with child-related information
|
// appSeal seals the application with child-related information
|
||||||
type appSeal struct {
|
type appSeal struct {
|
||||||
// application unique identifier
|
|
||||||
id *appID
|
|
||||||
// wayland socket path if mediated wayland is enabled
|
// wayland socket path if mediated wayland is enabled
|
||||||
wl string
|
wl string
|
||||||
// wait for wayland client to exit if mediated wayland is enabled,
|
// wait for wayland client to exit if mediated wayland is enabled,
|
||||||
// (wlDone == nil) determines whether mediated wayland setup is performed
|
// (wlDone == nil) determines whether mediated wayland setup is performed
|
||||||
wlDone chan struct{}
|
wlDone chan struct{}
|
||||||
|
|
||||||
|
// app unique ID string representation
|
||||||
|
id string
|
||||||
// freedesktop application ID
|
// freedesktop application ID
|
||||||
fid string
|
fid string
|
||||||
// argv to start process with in the final confined environment
|
// argv to start process with in the final confined environment
|
||||||
|
@ -76,10 +75,9 @@ func (seal *appSeal) shareAll(bus [2]*dbus.Config) error {
|
||||||
}
|
}
|
||||||
seal.shared = true
|
seal.shared = true
|
||||||
|
|
||||||
targetTmpdir := seal.shareTmpdirChild()
|
|
||||||
verbose.Printf("child tmpdir %q configured\n", targetTmpdir)
|
|
||||||
seal.shareRuntime()
|
|
||||||
seal.shareSystem()
|
seal.shareSystem()
|
||||||
|
seal.shareRuntime()
|
||||||
|
seal.sharePasswd()
|
||||||
if err := seal.shareDisplay(); err != nil {
|
if err := seal.shareDisplay(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -145,11 +145,12 @@ func doInit(fd uintptr) {
|
||||||
default:
|
default:
|
||||||
r = 255
|
r = 255
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
time.Sleep(residualProcessTimeout)
|
||||||
|
close(timeout)
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
go func() {
|
|
||||||
time.Sleep(residualProcessTimeout)
|
|
||||||
close(timeout)
|
|
||||||
}()
|
|
||||||
case <-done:
|
case <-done:
|
||||||
os.Exit(r)
|
os.Exit(r)
|
||||||
case <-timeout:
|
case <-timeout:
|
||||||
|
|
|
@ -68,7 +68,7 @@ func (s *simpleStore) mustPrintLauncherState(w **tabwriter.Writer, now time.Time
|
||||||
|
|
||||||
// write header when initialising
|
// write header when initialising
|
||||||
if !verbose.Get() {
|
if !verbose.Get() {
|
||||||
_, _ = fmt.Fprintln(*w, "\tUID\tPID\tUptime\tEnablements\tLauncher\tCommand")
|
_, _ = fmt.Fprintln(*w, "\tUID\tPID\tUptime\tEnablements\tMethod\tCommand")
|
||||||
} else {
|
} else {
|
||||||
// argv is emitted in body when verbose
|
// argv is emitted in body when verbose
|
||||||
_, _ = fmt.Fprintln(*w, "\tUID\tPID\tArgv")
|
_, _ = fmt.Fprintln(*w, "\tUID\tPID\tArgv")
|
||||||
|
@ -98,7 +98,7 @@ func (s *simpleStore) mustPrintLauncherState(w **tabwriter.Writer, now time.Time
|
||||||
|
|
||||||
if !verbose.Get() {
|
if !verbose.Get() {
|
||||||
_, _ = fmt.Fprintf(*w, "\t%s\t%d\t%s\t%s\t%s\t%s\n",
|
_, _ = fmt.Fprintf(*w, "\t%s\t%d\t%s\t%s\t%s\t%s\n",
|
||||||
s.path[len(s.path)-1], state.PID, now.Sub(state.Time).Round(time.Second).String(), strings.TrimPrefix(ets.String(), ", "), state.Launcher,
|
s.path[len(s.path)-1], state.PID, now.Sub(state.Time).Round(time.Second).String(), strings.TrimPrefix(ets.String(), ", "), state.Method,
|
||||||
state.Command)
|
state.Command)
|
||||||
} else {
|
} else {
|
||||||
// emit argv instead when verbose
|
// emit argv instead when verbose
|
||||||
|
|
|
@ -33,8 +33,8 @@ type State struct {
|
||||||
// capability enablements applied to child
|
// capability enablements applied to child
|
||||||
Capability system.Enablements
|
Capability system.Enablements
|
||||||
|
|
||||||
// resolved launcher path
|
// user switch method
|
||||||
Launcher string
|
Method string
|
||||||
// full argv whe launching
|
// full argv whe launching
|
||||||
Argv []string
|
Argv []string
|
||||||
// process start time
|
// process start time
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.ophivana.moe/cat/fortify/acl"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUpdatePerm(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
path string
|
||||||
|
perms []acl.Perm
|
||||||
|
}{
|
||||||
|
{"/run/user/1971/fortify", []acl.Perm{acl.Execute}},
|
||||||
|
{"/tmp/fortify.1971/tmpdir/150", []acl.Perm{acl.Read, acl.Write, acl.Execute}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.path+permSubTestSuffix(tc.perms), func(t *testing.T) {
|
||||||
|
sys := New(150)
|
||||||
|
sys.UpdatePerm(tc.path, tc.perms...)
|
||||||
|
(&tcOp{Process, tc.path}).test(t, sys.ops, []Op{&ACL{Process, tc.path, tc.perms}}, "UpdatePerm")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdatePermType(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
perms []acl.Perm
|
||||||
|
tcOp
|
||||||
|
}{
|
||||||
|
{[]acl.Perm{acl.Execute}, tcOp{User, "/tmp/fortify.1971/tmpdir"}},
|
||||||
|
{[]acl.Perm{acl.Read, acl.Write, acl.Execute}, tcOp{User, "/tmp/fortify.1971/tmpdir/150"}},
|
||||||
|
{[]acl.Perm{acl.Execute}, tcOp{Process, "/run/user/1971/fortify/fcb8a12f7c482d183ade8288c3de78b5"}},
|
||||||
|
{[]acl.Perm{acl.Read}, tcOp{Process, "/tmp/fortify.1971/fcb8a12f7c482d183ade8288c3de78b5/passwd"}},
|
||||||
|
{[]acl.Perm{acl.Read}, tcOp{Process, "/tmp/fortify.1971/fcb8a12f7c482d183ade8288c3de78b5/group"}},
|
||||||
|
{[]acl.Perm{acl.Read, acl.Write, acl.Execute}, tcOp{EWayland, "/run/user/1971/wayland-0"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.path+"_"+TypeString(tc.et)+permSubTestSuffix(tc.perms), func(t *testing.T) {
|
||||||
|
sys := New(150)
|
||||||
|
sys.UpdatePermType(tc.et, tc.path, tc.perms...)
|
||||||
|
tc.test(t, sys.ops, []Op{&ACL{tc.et, tc.path, tc.perms}}, "UpdatePermType")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestACL_String(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
want string
|
||||||
|
perms []acl.Perm
|
||||||
|
}{
|
||||||
|
{"---", []acl.Perm{}},
|
||||||
|
{"r--", []acl.Perm{acl.Read}},
|
||||||
|
{"-w-", []acl.Perm{acl.Write}},
|
||||||
|
{"--x", []acl.Perm{acl.Execute}},
|
||||||
|
{"rw-", []acl.Perm{acl.Read, acl.Write}},
|
||||||
|
{"r-x", []acl.Perm{acl.Read, acl.Execute}},
|
||||||
|
{"rwx", []acl.Perm{acl.Read, acl.Write, acl.Execute}},
|
||||||
|
{"rwx", []acl.Perm{acl.Read, acl.Write, acl.Write, acl.Execute}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.want, func(t *testing.T) {
|
||||||
|
a := &ACL{perms: tc.perms}
|
||||||
|
if got := a.String(); got != tc.want {
|
||||||
|
t.Errorf("String() = %v, want %v",
|
||||||
|
got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func permSubTestSuffix(perms []acl.Perm) (suffix string) {
|
||||||
|
for _, perm := range perms {
|
||||||
|
switch perm {
|
||||||
|
case acl.Read:
|
||||||
|
suffix += "_read"
|
||||||
|
case acl.Write:
|
||||||
|
suffix += "_write"
|
||||||
|
case acl.Execute:
|
||||||
|
suffix += "_execute"
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEnsure(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
perm os.FileMode
|
||||||
|
}{
|
||||||
|
{"/tmp/fortify.1971", 0701},
|
||||||
|
{"/tmp/fortify.1971/tmpdir", 0700},
|
||||||
|
{"/tmp/fortify.1971/tmpdir/150", 0700},
|
||||||
|
{"/run/user/1971/fortify", 0700},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name+"_"+tc.perm.String(), func(t *testing.T) {
|
||||||
|
sys := New(150)
|
||||||
|
sys.Ensure(tc.name, tc.perm)
|
||||||
|
(&tcOp{User, tc.name}).test(t, sys.ops, []Op{&Mkdir{User, tc.name, tc.perm, false}}, "Ensure")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEphemeral(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
perm os.FileMode
|
||||||
|
tcOp
|
||||||
|
}{
|
||||||
|
{0700, tcOp{Process, "/run/user/1971/fortify/ec07546a772a07cde87389afc84ffd13"}},
|
||||||
|
{0701, tcOp{Process, "/tmp/fortify.1971/ec07546a772a07cde87389afc84ffd13"}},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.path+"_"+tc.perm.String()+"_"+TypeString(tc.et), func(t *testing.T) {
|
||||||
|
sys := New(150)
|
||||||
|
sys.Ephemeral(tc.et, tc.path, tc.perm)
|
||||||
|
tc.test(t, sys.ops, []Op{&Mkdir{tc.et, tc.path, tc.perm, true}}, "Ephemeral")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMkdir_String(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
want string
|
||||||
|
ephemeral bool
|
||||||
|
et Enablement
|
||||||
|
}{
|
||||||
|
{"Ensure", false, User},
|
||||||
|
{"Ensure", false, Process},
|
||||||
|
{"Ensure", false, EWayland},
|
||||||
|
|
||||||
|
{"Wayland", true, EWayland},
|
||||||
|
{"X11", true, EX11},
|
||||||
|
{"D-Bus", true, EDBus},
|
||||||
|
{"PulseAudio", true, EPulse},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.want, func(t *testing.T) {
|
||||||
|
m := &Mkdir{
|
||||||
|
et: tc.et,
|
||||||
|
path: "/nonexistent",
|
||||||
|
perm: 0701,
|
||||||
|
ephemeral: tc.ephemeral,
|
||||||
|
}
|
||||||
|
want := "mode: " + os.FileMode(0701).String() + " type: " + tc.want + " path: \"/nonexistent\""
|
||||||
|
if got := m.String(); got != want {
|
||||||
|
t.Errorf("String() = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package system
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
type tcOp struct {
|
||||||
|
et Enablement
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// test an instance of the Op interface
|
||||||
|
func (ptc tcOp) test(t *testing.T, gotOps []Op, wantOps []Op, fn string) {
|
||||||
|
if len(gotOps) != len(wantOps) {
|
||||||
|
t.Errorf("%s: inserted %v Ops, want %v", fn,
|
||||||
|
len(gotOps), len(wantOps))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("path", func(t *testing.T) {
|
||||||
|
if len(gotOps) > 0 {
|
||||||
|
if got := gotOps[0].Path(); got != ptc.path {
|
||||||
|
t.Errorf("Path() = %q, want %q",
|
||||||
|
got, ptc.path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
for i := range gotOps {
|
||||||
|
o := gotOps[i]
|
||||||
|
|
||||||
|
t.Run("is", func(t *testing.T) {
|
||||||
|
if !o.Is(o) {
|
||||||
|
t.Errorf("Is returned false on self")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !o.Is(wantOps[i]) {
|
||||||
|
t.Errorf("%s: inserted %#v, want %#v",
|
||||||
|
fn,
|
||||||
|
o, wantOps[i])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("criteria", func(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
ec *Criteria
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"nil", newCriteria(), ptc.et != User},
|
||||||
|
{"self", newCriteria(ptc.et), true},
|
||||||
|
{"all", newCriteria(EWayland, EX11, EDBus, EPulse, User, Process), true},
|
||||||
|
{"enablements", newCriteria(EWayland, EX11, EDBus, EPulse), ptc.et != User && ptc.et != Process},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if got := tc.ec.hasType(o); got != tc.want {
|
||||||
|
t.Errorf("hasType: got %v, want %v",
|
||||||
|
got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCriteria(labels ...Enablement) *Criteria {
|
||||||
|
ec := new(Criteria)
|
||||||
|
if len(labels) == 0 {
|
||||||
|
return ec
|
||||||
|
}
|
||||||
|
|
||||||
|
ec.Enablements = new(Enablements)
|
||||||
|
for _, e := range labels {
|
||||||
|
ec.Set(e)
|
||||||
|
}
|
||||||
|
return ec
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package system_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.ophivana.moe/cat/fortify/internal/system"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
uid int
|
||||||
|
}{
|
||||||
|
{150},
|
||||||
|
{149},
|
||||||
|
{148},
|
||||||
|
{147},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run("sys initialised with uid "+strconv.Itoa(tc.uid), func(t *testing.T) {
|
||||||
|
if got := system.New(tc.uid); got.UID() != tc.uid {
|
||||||
|
t.Errorf("New(%d) uid = %d, want %d",
|
||||||
|
tc.uid,
|
||||||
|
got.UID(), tc.uid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTypeString(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
e system.Enablement
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{system.EWayland, system.EWayland.String()},
|
||||||
|
{system.EX11, system.EX11.String()},
|
||||||
|
{system.EDBus, system.EDBus.String()},
|
||||||
|
{system.EPulse, system.EPulse.String()},
|
||||||
|
{system.User, "User"},
|
||||||
|
{system.Process, "Process"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run("label type string "+tc.want, func(t *testing.T) {
|
||||||
|
if got := system.TypeString(tc.e); got != tc.want {
|
||||||
|
t.Errorf("TypeString(%d) = %v, want %v",
|
||||||
|
tc.e,
|
||||||
|
got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,167 @@
|
||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.ophivana.moe/cat/fortify/acl"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCopyFile(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
dst, src string
|
||||||
|
}{
|
||||||
|
{"/tmp/fortify.1971/f587afe9fce3c8e1ad5b64deb6c41ad5/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie"},
|
||||||
|
{"/tmp/fortify.1971/62154f708b5184ab01f9dcc2bbe7a33b/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie"},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run("copy file "+tc.dst+" from "+tc.src, func(t *testing.T) {
|
||||||
|
sys := New(150)
|
||||||
|
sys.CopyFile(tc.dst, tc.src)
|
||||||
|
(&tcOp{Process, tc.src}).test(t, sys.ops, []Op{
|
||||||
|
&Tmpfile{Process, tmpfileCopy, tc.dst, tc.src},
|
||||||
|
&ACL{Process, tc.dst, []acl.Perm{acl.Read}},
|
||||||
|
}, "CopyFile")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCopyFileType(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tcOp
|
||||||
|
dst string
|
||||||
|
}{
|
||||||
|
{tcOp{User, "/tmp/fortify.1971/f587afe9fce3c8e1ad5b64deb6c41ad5/pulse-cookie"}, "/home/ophestra/xdg/config/pulse/cookie"},
|
||||||
|
{tcOp{Process, "/tmp/fortify.1971/62154f708b5184ab01f9dcc2bbe7a33b/pulse-cookie"}, "/home/ophestra/xdg/config/pulse/cookie"},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run("copy file "+tc.dst+" from "+tc.path+" with type "+TypeString(tc.et), func(t *testing.T) {
|
||||||
|
sys := New(150)
|
||||||
|
sys.CopyFileType(tc.et, tc.dst, tc.path)
|
||||||
|
tc.test(t, sys.ops, []Op{
|
||||||
|
&Tmpfile{tc.et, tmpfileCopy, tc.dst, tc.path},
|
||||||
|
&ACL{tc.et, tc.dst, []acl.Perm{acl.Read}},
|
||||||
|
}, "CopyFileType")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLink(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
dst, src string
|
||||||
|
}{
|
||||||
|
{"/tmp/fortify.1971/f587afe9fce3c8e1ad5b64deb6c41ad5/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie"},
|
||||||
|
{"/tmp/fortify.1971/62154f708b5184ab01f9dcc2bbe7a33b/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie"},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run("link file "+tc.dst+" from "+tc.src, func(t *testing.T) {
|
||||||
|
sys := New(150)
|
||||||
|
sys.Link(tc.src, tc.dst)
|
||||||
|
(&tcOp{Process, tc.src}).test(t, sys.ops, []Op{
|
||||||
|
&Tmpfile{Process, tmpfileLink, tc.dst, tc.src},
|
||||||
|
}, "Link")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLinkFileType(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tcOp
|
||||||
|
dst string
|
||||||
|
}{
|
||||||
|
{tcOp{User, "/tmp/fortify.1971/f587afe9fce3c8e1ad5b64deb6c41ad5/pulse-cookie"}, "/home/ophestra/xdg/config/pulse/cookie"},
|
||||||
|
{tcOp{Process, "/tmp/fortify.1971/62154f708b5184ab01f9dcc2bbe7a33b/pulse-cookie"}, "/home/ophestra/xdg/config/pulse/cookie"},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run("link file "+tc.dst+" from "+tc.path+" with type "+TypeString(tc.et), func(t *testing.T) {
|
||||||
|
sys := New(150)
|
||||||
|
sys.LinkFileType(tc.et, tc.path, tc.dst)
|
||||||
|
tc.test(t, sys.ops, []Op{
|
||||||
|
&Tmpfile{tc.et, tmpfileLink, tc.dst, tc.path},
|
||||||
|
}, "LinkFileType")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWrite(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
dst, src string
|
||||||
|
}{
|
||||||
|
{"/etc/passwd", "chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n"},
|
||||||
|
{"/etc/group", "fortify:x:65534:\n"},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run("write "+strconv.Itoa(len(tc.src))+" bytes to "+tc.dst, func(t *testing.T) {
|
||||||
|
sys := New(150)
|
||||||
|
sys.Write(tc.dst, tc.src)
|
||||||
|
(&tcOp{Process, "(" + strconv.Itoa(len(tc.src)) + " bytes of data)"}).test(t, sys.ops, []Op{
|
||||||
|
&Tmpfile{Process, tmpfileWrite, tc.dst, tc.src},
|
||||||
|
&ACL{Process, tc.dst, []acl.Perm{acl.Read}},
|
||||||
|
}, "Write")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteType(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
et Enablement
|
||||||
|
dst, src string
|
||||||
|
}{
|
||||||
|
{Process, "/etc/passwd", "chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n"},
|
||||||
|
{Process, "/etc/group", "fortify:x:65534:\n"},
|
||||||
|
{User, "/etc/passwd", "chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n"},
|
||||||
|
{User, "/etc/group", "fortify:x:65534:\n"},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run("write "+strconv.Itoa(len(tc.src))+" bytes to "+tc.dst+" with type "+TypeString(tc.et), func(t *testing.T) {
|
||||||
|
sys := New(150)
|
||||||
|
sys.WriteType(tc.et, tc.dst, tc.src)
|
||||||
|
(&tcOp{tc.et, "(" + strconv.Itoa(len(tc.src)) + " bytes of data)"}).test(t, sys.ops, []Op{
|
||||||
|
&Tmpfile{tc.et, tmpfileWrite, tc.dst, tc.src},
|
||||||
|
&ACL{tc.et, tc.dst, []acl.Perm{acl.Read}},
|
||||||
|
}, "WriteType")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTmpfile_String(t *testing.T) {
|
||||||
|
t.Run("invalid method panic", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
wantPanic := "invalid tmpfile method 255"
|
||||||
|
if r := recover(); r != wantPanic {
|
||||||
|
t.Errorf("String() panic = %v, want %v",
|
||||||
|
r, wantPanic)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_ = (&Tmpfile{method: 255}).String()
|
||||||
|
})
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
method uint8
|
||||||
|
dst, src string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{tmpfileCopy, "/tmp/fortify.1971/4b6bdc9182fb2f1d3a965c5fa8b9b66e/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie",
|
||||||
|
`"/tmp/fortify.1971/4b6bdc9182fb2f1d3a965c5fa8b9b66e/pulse-cookie" from "/home/ophestra/xdg/config/pulse/cookie"`},
|
||||||
|
{tmpfileLink, "/run/user/1971/fortify/4b6bdc9182fb2f1d3a965c5fa8b9b66e/wayland", "/run/user/1971/wayland-0",
|
||||||
|
`"/run/user/1971/fortify/4b6bdc9182fb2f1d3a965c5fa8b9b66e/wayland" from "/run/user/1971/wayland-0"`},
|
||||||
|
{tmpfileLink, "/run/user/1971/fortify/4b6bdc9182fb2f1d3a965c5fa8b9b66e/pulse", "/run/user/1971/pulse/native",
|
||||||
|
`"/run/user/1971/fortify/4b6bdc9182fb2f1d3a965c5fa8b9b66e/pulse" from "/run/user/1971/pulse/native"`},
|
||||||
|
{tmpfileWrite, "/tmp/fortify.1971/4b6bdc9182fb2f1d3a965c5fa8b9b66e/passwd", "chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n",
|
||||||
|
`75 bytes of data to "/tmp/fortify.1971/4b6bdc9182fb2f1d3a965c5fa8b9b66e/passwd"`},
|
||||||
|
{tmpfileWrite, "/tmp/fortify.1971/4b6bdc9182fb2f1d3a965c5fa8b9b66e/group", "fortify:x:65534:\n",
|
||||||
|
`17 bytes of data to "/tmp/fortify.1971/4b6bdc9182fb2f1d3a965c5fa8b9b66e/group"`},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.want, func(t *testing.T) {
|
||||||
|
if got := (&Tmpfile{
|
||||||
|
method: tc.method,
|
||||||
|
dst: tc.dst,
|
||||||
|
src: tc.src,
|
||||||
|
}).String(); got != tc.want {
|
||||||
|
t.Errorf("String() = %v, want %v", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestChangeHosts(t *testing.T) {
|
||||||
|
testCases := []string{"chronos", "keyring", "cat", "kbd", "yonah"}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run("append ChangeHosts operation for "+tc, func(t *testing.T) {
|
||||||
|
sys := New(150)
|
||||||
|
sys.ChangeHosts(tc)
|
||||||
|
(&tcOp{EX11, tc}).test(t, sys.ops, []Op{
|
||||||
|
XHost(tc),
|
||||||
|
}, "ChangeHosts")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestXHost_String(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
username string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"chronos", "SI:localuser:chronos"},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.want, func(t *testing.T) {
|
||||||
|
if got := XHost(tc.username).String(); got != tc.want {
|
||||||
|
t.Errorf("String() = %v, want %v", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
8
main.go
8
main.go
|
@ -54,8 +54,10 @@ func main() {
|
||||||
|
|
||||||
// invoke app
|
// invoke app
|
||||||
r := 1
|
r := 1
|
||||||
a := app.New()
|
a, err := app.New()
|
||||||
if err := a.Seal(loadConfig()); err != nil {
|
if err != nil {
|
||||||
|
fatalf("cannot create app: %s\n", err)
|
||||||
|
} else if err = a.Seal(loadConfig()); err != nil {
|
||||||
logBaseError(err, "fortify: cannot seal app:")
|
logBaseError(err, "fortify: cannot seal app:")
|
||||||
} else if err = a.Start(); err != nil {
|
} else if err = a.Start(); err != nil {
|
||||||
logBaseError(err, "fortify: cannot start app:")
|
logBaseError(err, "fortify: cannot start app:")
|
||||||
|
@ -65,7 +67,7 @@ func main() {
|
||||||
}
|
}
|
||||||
logWaitError(err)
|
logWaitError(err)
|
||||||
}
|
}
|
||||||
if err := a.WaitErr(); err != nil {
|
if err = a.WaitErr(); err != nil {
|
||||||
fmt.Println("fortify: inner wait failed:", err)
|
fmt.Println("fortify: inner wait failed:", err)
|
||||||
}
|
}
|
||||||
os.Exit(r)
|
os.Exit(r)
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
buildGoModule rec {
|
buildGoModule rec {
|
||||||
pname = "fortify";
|
pname = "fortify";
|
||||||
version = "0.0.4";
|
version = "0.0.6";
|
||||||
|
|
||||||
src = ./.;
|
src = ./.;
|
||||||
vendorHash = null;
|
vendorHash = null;
|
||||||
|
|
Loading…
Reference in New Issue