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
|
||||
run: >-
|
||||
sudo apt-get update &&
|
||||
sudo apt-get install -y
|
||||
apt-get update &&
|
||||
apt-get install -y
|
||||
gcc
|
||||
pkg-config
|
||||
libacl1-dev
|
||||
|
|
|
@ -20,8 +20,8 @@ jobs:
|
|||
|
||||
- name: Get dependencies
|
||||
run: >-
|
||||
sudo apt-get update &&
|
||||
sudo apt-get install -y
|
||||
apt-get update &&
|
||||
apt-get install -y
|
||||
gcc
|
||||
pkg-config
|
||||
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)
|
||||
|
||||
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.
|
||||
|
||||
Why would you want this?
|
||||
|
@ -12,7 +12,7 @@ Why would you want this?
|
|||
|
||||
- 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:
|
||||
|
||||
|
|
|
@ -34,6 +34,12 @@
|
|||
|
||||
devShells = forAllSystems (system: {
|
||||
default = nixpkgsFor.${system}.mkShell {
|
||||
buildInputs =
|
||||
with nixpkgsFor.${system};
|
||||
self.packages.${system}.fortify.buildInputs;
|
||||
};
|
||||
|
||||
withPackage = nixpkgsFor.${system}.mkShell {
|
||||
buildInputs =
|
||||
with nixpkgsFor.${system};
|
||||
self.packages.${system}.fortify.buildInputs ++ [ self.packages.${system}.fortify ];
|
||||
|
|
|
@ -7,26 +7,39 @@ import (
|
|||
)
|
||||
|
||||
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
|
||||
// Wait waits for App's process to exit and reverts system setup.
|
||||
Wait() (int, error)
|
||||
// WaitErr returns error returned by the underlying wait syscall.
|
||||
WaitErr() error
|
||||
|
||||
Seal(config *Config) error
|
||||
String() string
|
||||
}
|
||||
|
||||
type app struct {
|
||||
// application unique identifier
|
||||
id *ID
|
||||
// underlying user switcher process
|
||||
cmd *exec.Cmd
|
||||
// child process related information
|
||||
seal *appSeal
|
||||
// underlying fortified child process
|
||||
cmd *exec.Cmd
|
||||
|
||||
// wayland connection if wayland mediation is enabled
|
||||
wayland *net.UnixConn
|
||||
// error returned waiting for process
|
||||
wait error
|
||||
waitErr error
|
||||
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func (a *app) ID() ID {
|
||||
return *a.id
|
||||
}
|
||||
|
||||
func (a *app) String() string {
|
||||
if a == nil {
|
||||
return "(invalid fortified app)"
|
||||
|
@ -47,9 +60,11 @@ func (a *app) String() string {
|
|||
}
|
||||
|
||||
func (a *app) WaitErr() error {
|
||||
return a.wait
|
||||
return a.waitErr
|
||||
}
|
||||
|
||||
func New() App {
|
||||
return new(app)
|
||||
func New() (App, error) {
|
||||
a := new(app)
|
||||
a.id = new(ID)
|
||||
return a, newAppID(a.id)
|
||||
}
|
||||
|
|
|
@ -5,14 +5,13 @@ import (
|
|||
"encoding/hex"
|
||||
)
|
||||
|
||||
type appID [16]byte
|
||||
type ID [16]byte
|
||||
|
||||
func (a *appID) String() string {
|
||||
func (a *ID) String() string {
|
||||
return hex.EncodeToString(a[:])
|
||||
}
|
||||
|
||||
func newAppID() (*appID, error) {
|
||||
a := &appID{}
|
||||
_, err := rand.Read(a[:])
|
||||
return a, err
|
||||
func newAppID(id *ID) error {
|
||||
_, err := rand.Read(id[:])
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -21,6 +21,11 @@ const (
|
|||
LaunchMethodMachineCtl
|
||||
)
|
||||
|
||||
var method = [...]string{
|
||||
LaunchMethodSudo: "sudo",
|
||||
LaunchMethodMachineCtl: "systemd",
|
||||
}
|
||||
|
||||
var (
|
||||
ErrConfig = errors.New("no configuration to seal")
|
||||
ErrUser = errors.New("unknown user")
|
||||
|
@ -48,24 +53,17 @@ func (a *app) Seal(config *Config) error {
|
|||
// create seal
|
||||
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
|
||||
seal.SystemConstants = internal.GetSC()
|
||||
|
||||
// pass through config values
|
||||
seal.id = a.id.String()
|
||||
seal.fid = config.ID
|
||||
seal.command = config.Command
|
||||
|
||||
// parses launch method text and looks up tool path
|
||||
switch config.Method {
|
||||
case "sudo":
|
||||
case method[LaunchMethodSudo]:
|
||||
seal.launchOption = LaunchMethodSudo
|
||||
if sudoPath, err := exec.LookPath("sudo"); err != nil {
|
||||
return fmsg.WrapError(ErrSudo,
|
||||
|
@ -73,7 +71,7 @@ func (a *app) Seal(config *Config) error {
|
|||
} else {
|
||||
seal.toolPath = sudoPath
|
||||
}
|
||||
case "systemd":
|
||||
case method[LaunchMethodMachineCtl]:
|
||||
seal.launchOption = LaunchMethodMachineCtl
|
||||
if !internal.SdBootedV {
|
||||
return fmsg.WrapError(ErrSystemd,
|
||||
|
|
|
@ -31,17 +31,8 @@ func (seal *appSeal) shareRuntime() {
|
|||
// ensure runtime directory ACL (e.g. `/run/user/%d`)
|
||||
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`)
|
||||
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.UpdatePerm(seal.shareLocal, acl.Execute)
|
||||
}
|
||||
|
|
|
@ -14,6 +14,31 @@ const (
|
|||
|
||||
// shareSystem queues various system-related actions
|
||||
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
|
||||
sh := "/bin/sh"
|
||||
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(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,
|
||||
Command: a.seal.command,
|
||||
Capability: a.seal.et,
|
||||
Launcher: a.seal.toolPath,
|
||||
Method: method[a.seal.launchOption],
|
||||
Argv: a.cmd.Args,
|
||||
Time: startTime,
|
||||
}
|
||||
|
@ -173,7 +173,7 @@ func (a *app) Wait() (int, error) {
|
|||
var exitError *exec.ExitError
|
||||
if !errors.As(err, &exitError) {
|
||||
// should be unreachable
|
||||
a.wait = err
|
||||
a.waitErr = err
|
||||
}
|
||||
|
||||
// store non-zero return code
|
||||
|
|
|
@ -8,19 +8,18 @@ import (
|
|||
"git.ophivana.moe/cat/fortify/internal"
|
||||
"git.ophivana.moe/cat/fortify/internal/state"
|
||||
"git.ophivana.moe/cat/fortify/internal/system"
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
)
|
||||
|
||||
// appSeal seals the application with child-related information
|
||||
type appSeal struct {
|
||||
// application unique identifier
|
||||
id *appID
|
||||
// wayland socket path if mediated wayland is enabled
|
||||
wl string
|
||||
// wait for wayland client to exit if mediated wayland is enabled,
|
||||
// (wlDone == nil) determines whether mediated wayland setup is performed
|
||||
wlDone chan struct{}
|
||||
|
||||
// app unique ID string representation
|
||||
id string
|
||||
// freedesktop application ID
|
||||
fid string
|
||||
// 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
|
||||
|
||||
targetTmpdir := seal.shareTmpdirChild()
|
||||
verbose.Printf("child tmpdir %q configured\n", targetTmpdir)
|
||||
seal.shareRuntime()
|
||||
seal.shareSystem()
|
||||
seal.shareRuntime()
|
||||
seal.sharePasswd()
|
||||
if err := seal.shareDisplay(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -145,11 +145,12 @@ func doInit(fd uintptr) {
|
|||
default:
|
||||
r = 255
|
||||
}
|
||||
|
||||
go func() {
|
||||
time.Sleep(residualProcessTimeout)
|
||||
close(timeout)
|
||||
}()
|
||||
}
|
||||
go func() {
|
||||
time.Sleep(residualProcessTimeout)
|
||||
close(timeout)
|
||||
}()
|
||||
case <-done:
|
||||
os.Exit(r)
|
||||
case <-timeout:
|
||||
|
|
|
@ -68,7 +68,7 @@ func (s *simpleStore) mustPrintLauncherState(w **tabwriter.Writer, now time.Time
|
|||
|
||||
// write header when initialising
|
||||
if !verbose.Get() {
|
||||
_, _ = fmt.Fprintln(*w, "\tUID\tPID\tUptime\tEnablements\tLauncher\tCommand")
|
||||
_, _ = fmt.Fprintln(*w, "\tUID\tPID\tUptime\tEnablements\tMethod\tCommand")
|
||||
} else {
|
||||
// argv is emitted in body when verbose
|
||||
_, _ = fmt.Fprintln(*w, "\tUID\tPID\tArgv")
|
||||
|
@ -98,7 +98,7 @@ func (s *simpleStore) mustPrintLauncherState(w **tabwriter.Writer, now time.Time
|
|||
|
||||
if !verbose.Get() {
|
||||
_, _ = 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)
|
||||
} else {
|
||||
// emit argv instead when verbose
|
||||
|
|
|
@ -33,8 +33,8 @@ type State struct {
|
|||
// capability enablements applied to child
|
||||
Capability system.Enablements
|
||||
|
||||
// resolved launcher path
|
||||
Launcher string
|
||||
// user switch method
|
||||
Method string
|
||||
// full argv whe launching
|
||||
Argv []string
|
||||
// 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
|
||||
r := 1
|
||||
a := app.New()
|
||||
if err := a.Seal(loadConfig()); err != nil {
|
||||
a, err := app.New()
|
||||
if err != nil {
|
||||
fatalf("cannot create app: %s\n", err)
|
||||
} else if err = a.Seal(loadConfig()); err != nil {
|
||||
logBaseError(err, "fortify: cannot seal app:")
|
||||
} else if err = a.Start(); err != nil {
|
||||
logBaseError(err, "fortify: cannot start app:")
|
||||
|
@ -65,7 +67,7 @@ func main() {
|
|||
}
|
||||
logWaitError(err)
|
||||
}
|
||||
if err := a.WaitErr(); err != nil {
|
||||
if err = a.WaitErr(); err != nil {
|
||||
fmt.Println("fortify: inner wait failed:", err)
|
||||
}
|
||||
os.Exit(r)
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
buildGoModule rec {
|
||||
pname = "fortify";
|
||||
version = "0.0.4";
|
||||
version = "0.0.6";
|
||||
|
||||
src = ./.;
|
||||
vendorHash = null;
|
||||
|
|
Loading…
Reference in New Issue