The engine is built without cgo, so a check by name missed a root group a name service holds or one number carrying another name: the root groups are resolved to numbers (now with kmem and shadow) and a number named nothing is refused for containers too. A group without write is granted nothing (§2). The look reads each path off the lock within five seconds, so a dead network mount cannot stop every health statement. A grant's account may not start with a dash. A group a record leaves for another asker is handed to it, so it is still taken back when the last asker goes (§5). The scheduler skips paths issue 339's guard refuses.
560 lines
23 KiB
Go
560 lines
23 KiB
Go
package apply
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-host/internal/accounts"
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
"github.com/novox/mesh-host/internal/store"
|
|
)
|
|
|
|
// Defends novox/hq ADR 0312 and issue 537: a read-write access grants its path's own group to the containers and
|
|
// the account the controller names, and the path is never changed; a read access grants nothing; the root group,
|
|
// and a group whose members act as root, are refused by name; what was granted is taken back as ADR 0252 takes
|
|
// back; a container is recreated with the group, and a unit of the machine's own manager running as the account
|
|
// is restarted once the account holds it.
|
|
|
|
// grantMachine is the group database of groupDB, a container runtime that keeps each container's spec label and
|
|
// the arguments it was run with, and a service manager that notes, at each restart, the groups the account holds
|
|
// in the database then — which is what the machine's own manager gives the unit's new process.
|
|
type grantMachine struct {
|
|
*groupDB
|
|
containers map[string]string
|
|
runs map[string][]string
|
|
restarts []string
|
|
}
|
|
|
|
func newGrantMachine(account string, in []string, exists ...string) *grantMachine {
|
|
g := newGroupDB(in, exists...)
|
|
g.account = account
|
|
return &grantMachine{groupDB: g, containers: map[string]string{}, runs: map[string][]string{}}
|
|
}
|
|
|
|
func (m *grantMachine) run(ctx context.Context, name string, args ...string) (string, error) {
|
|
a := oldShape(name, args)
|
|
switch name {
|
|
case "docker":
|
|
m.asked = append(m.asked, name+" "+strings.Join(a, " "))
|
|
switch a[0] {
|
|
case "info":
|
|
return "27.0\n", nil
|
|
case "container":
|
|
spec, there := m.containers[a[len(a)-1]]
|
|
if !there {
|
|
return "", errors.New("no such container")
|
|
}
|
|
return "true\t" + spec, nil
|
|
case "rm":
|
|
delete(m.containers, a[len(a)-1])
|
|
case "run":
|
|
var named, spec string
|
|
for i := 0; i+1 < len(a); i++ {
|
|
switch {
|
|
case a[i] == "--name":
|
|
named = a[i+1]
|
|
case a[i] == "--label" && strings.HasPrefix(a[i+1], specLabel+"="):
|
|
spec = strings.TrimPrefix(a[i+1], specLabel+"=")
|
|
}
|
|
}
|
|
m.containers[named] = spec
|
|
m.runs[named] = a
|
|
return "deadbeef\n", nil
|
|
}
|
|
return "", nil
|
|
case "systemctl":
|
|
m.asked = append(m.asked, name+" "+strings.Join(a, " "))
|
|
if a[0] == "restart" {
|
|
m.restarts = append(m.restarts, a[1]+": "+m.groups())
|
|
}
|
|
return "", nil
|
|
}
|
|
return m.groupDB.run(ctx, name, args...)
|
|
}
|
|
|
|
// groupOfPaths stands in for the machine's group database and the paths' groups for the length of a test.
|
|
func groupOfPaths(t *testing.T, groups map[string]PathGroup) {
|
|
t.Helper()
|
|
was := GroupOfPath
|
|
GroupOfPath = func(path string) (PathGroup, error) {
|
|
if _, err := os.Stat(path); err != nil {
|
|
return PathGroup{}, err
|
|
}
|
|
g, ok := groups[path]
|
|
if !ok {
|
|
return PathGroup{}, errors.New("no group in this test for " + path)
|
|
}
|
|
return g, nil
|
|
}
|
|
t.Cleanup(func() { GroupOfPath = was })
|
|
rootGroupsAre(t, map[int]string{0: "root", 10: "wheel", 968: "docker"})
|
|
}
|
|
|
|
// rootGroupsAre stands in for the numbers this machine gives the groups whose members act as root.
|
|
func rootGroupsAre(t *testing.T, ids map[int]string) {
|
|
t.Helper()
|
|
was := RootGroupIDs
|
|
RootGroupIDs = func() map[int]string { return ids }
|
|
t.Cleanup(func() { RootGroupIDs = was })
|
|
}
|
|
|
|
func accessGranting(id, path, account string, containers ...string) string {
|
|
grant := `"grant":{"account":"` + account + `"`
|
|
if len(containers) > 0 {
|
|
grant += `,"containers":["` + strings.Join(containers, `","`) + `"]`
|
|
}
|
|
return `{"id":"` + id + `","type":"access","path":"` + path + `","mode":"read-write",` + grant + `}}`
|
|
}
|
|
|
|
func mounting(id, path string) string {
|
|
return `{"id":"` + id + `","type":"container","name":"` + strings.ReplaceAll(id, ".", "-") + `","image":"` + pinned +
|
|
`","volumes":["` + path + `:/library"]}`
|
|
}
|
|
|
|
func applyGrants(t *testing.T, m *grantMachine, known store.State, resources ...string) (Report, store.State, error) {
|
|
t.Helper()
|
|
return Apply(context.Background(), archHost(t), parse(t, `{"declaration":1,"resources":[`+
|
|
strings.Join(resources, ",")+`]}`), known, store.OriginDeclared, m.run, nil, nil)
|
|
}
|
|
|
|
func statOf(t *testing.T, path string) string {
|
|
t.Helper()
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
uid, gid, _ := OwnerOf(info)
|
|
return fmt.Sprintf("%s %d:%d %s", info.Mode(), uid, gid, info.ModTime())
|
|
}
|
|
|
|
func TestAReadWriteAccessGrantsItsPathsGroupToTheContainerAndTheAccountAndLeavesThePath(t *testing.T) {
|
|
library := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(library, "album"), []byte("x"), 0o640); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
groupOfPaths(t, map[string]PathGroup{library: {GID: 1001, Name: "media", Writable: true}})
|
|
before, beforeInside := statOf(t, library), statOf(t, filepath.Join(library, "album"))
|
|
m := newGrantMachine("operator", []string{"operator"}, "media")
|
|
|
|
report, state, err := applyGrants(t, m, store.State{},
|
|
accessGranting("importer.music", library, "operator", "importer.app"), mounting("importer.app", library))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(" "+m.groups()+" ", " media ") {
|
|
t.Fatalf("the account was not put in the path's group: %q", m.groups())
|
|
}
|
|
if run := strings.Join(m.runs["importer-app"], " "); !strings.Contains(run, "--group-add 1001") {
|
|
t.Fatalf("the container was not created with the path's group: %s", run)
|
|
}
|
|
o := outcomeOf(report, "importer.music")
|
|
if o.Action != "updated" || !strings.Contains(o.Detail, "grants media (gid 1001)") ||
|
|
!strings.Contains(o.Detail, "put operator in media") {
|
|
t.Errorf("the access did not say what it granted: %+v", o)
|
|
}
|
|
if rec, _ := state.Find("importer.music"); rec.User != "operator" || strings.Join(rec.Groups, " ") != "media" {
|
|
t.Errorf("the access did not record the group it put the account in: %+v", rec)
|
|
}
|
|
// The path, and what is under it, exactly as they were (ADR 0312 §2).
|
|
if after := statOf(t, library); after != before {
|
|
t.Errorf("the accessed path changed: %s, was %s", after, before)
|
|
}
|
|
if after := statOf(t, filepath.Join(library, "album")); after != beforeInside {
|
|
t.Errorf("a file under the accessed path changed: %s, was %s", after, beforeInside)
|
|
}
|
|
for _, asked := range m.asked {
|
|
for _, verb := range []string{"chown", "chmod", "chgrp", "setfacl"} {
|
|
if strings.HasPrefix(asked, verb) || strings.Contains(asked, " "+verb+" ") {
|
|
t.Errorf("the engine asked the machine to change something: %q", asked)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Applied again: nothing moves, and nothing is recreated.
|
|
m.runs = map[string][]string{}
|
|
report, _, err = applyGrants(t, m, state,
|
|
accessGranting("importer.music", library, "operator", "importer.app"), mounting("importer.app", library))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if o := outcomeOf(report, "importer.music"); o.Action != "unchanged" {
|
|
t.Errorf("a second apply moved the access: %+v", o)
|
|
}
|
|
if len(m.runs) != 0 {
|
|
t.Errorf("a second apply recreated the container: %v", m.runs)
|
|
}
|
|
}
|
|
|
|
func TestAReadAccessGrantsNothing(t *testing.T) {
|
|
library := t.TempDir()
|
|
groupOfPaths(t, map[string]PathGroup{library: {GID: 1001, Name: "media", Writable: true}})
|
|
|
|
// A grant on a read access is refused whole: the controller asked for something the decision forbids.
|
|
_, err := declaration.Parse([]byte(`{"declaration":1,"resources":[{"id":"player.music","type":"access","path":"` +
|
|
library + `","mode":"read","grant":{"account":"operator"}}]}`))
|
|
if err == nil || !strings.Contains(err.Error(), "a read access grants nothing") {
|
|
t.Fatalf("a grant on a read access was not refused by name: %v", err)
|
|
}
|
|
|
|
m := newGrantMachine("operator", []string{"operator"}, "media")
|
|
report, state, err := applyGrants(t, m, store.State{},
|
|
`{"id":"player.music","type":"access","path":"`+library+`","mode":"read"}`, mounting("player.app", library))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(m.groups(), "media") {
|
|
t.Errorf("a read access put the account in a group: %q", m.groups())
|
|
}
|
|
if run := strings.Join(m.runs["player-app"], " "); strings.Contains(run, "--group-add") {
|
|
t.Errorf("a read access gave its container a group: %s", run)
|
|
}
|
|
if o := outcomeOf(report, "player.music"); o.Action != "unchanged" {
|
|
t.Errorf("a read access moved something: %+v", o)
|
|
}
|
|
if rec, _ := state.Find("player.music"); len(rec.Groups) != 0 || rec.User != "" {
|
|
t.Errorf("a read access recorded a grant: %+v", rec)
|
|
}
|
|
}
|
|
|
|
func TestAnAccessWhoseGroupCannotWriteGrantsNothingAndSaysTheOperatorDecides(t *testing.T) {
|
|
spool := t.TempDir()
|
|
groupOfPaths(t, map[string]PathGroup{spool: {GID: 1002, Name: "downloads", Writable: false}})
|
|
before := statOf(t, spool)
|
|
m := newGrantMachine("operator", nil, "downloads")
|
|
|
|
report, _, err := applyGrants(t, m, store.State{},
|
|
accessGranting("nzbget.downloads", spool, "operator", "nzbget.app"), mounting("nzbget.app", spool))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
o := outcomeOf(report, "nzbget.downloads")
|
|
if !strings.Contains(o.Detail, "cannot write it, so nothing is granted") || !strings.Contains(o.Detail, "the operator decides") ||
|
|
!strings.Contains(o.Detail, "nothing was changed") {
|
|
t.Errorf("the access did not say its group cannot write and that the operator decides: %+v", o)
|
|
}
|
|
// Nothing is given (ADR 0312 §2): no account group, no container group.
|
|
if strings.Contains(m.groups(), "downloads") {
|
|
t.Errorf("a group that cannot write was granted to the account: %q", m.groups())
|
|
}
|
|
if run := strings.Join(m.runs["nzbget-app"], " "); strings.Contains(run, "--group-add") {
|
|
t.Errorf("a group that cannot write was granted to the container: %s", run)
|
|
}
|
|
if after := statOf(t, spool); after != before {
|
|
t.Errorf("the engine changed a path whose group cannot write: %s, was %s", after, before)
|
|
}
|
|
// The words the look states it unhealthy with: the remedy is the operator's, and the path is never in them.
|
|
words := CannotWriteWords(PathGroup{GID: 1002, Name: "downloads"})
|
|
if !strings.Contains(words, "the operator's to decide") || strings.Contains(words, spool) {
|
|
t.Errorf("the unhealthy reason is %q", words)
|
|
}
|
|
}
|
|
|
|
func TestTheRootGroupIsNeverGranted(t *testing.T) {
|
|
for _, g := range []PathGroup{{GID: 0, Name: "root", Writable: true}, {GID: 968, Name: "docker", Writable: true},
|
|
{GID: 10, Name: "wheel", Writable: true}, {GID: 42, Name: "shadow", Writable: true},
|
|
// A number the root groups have here under another name — judged by the number, never the name.
|
|
{GID: 968, Name: "containers", Writable: true},
|
|
// A number the machine names nothing for: unjudged, so never granted, to a container either.
|
|
{GID: 4242, Name: "", Writable: true}} {
|
|
want := g.Name
|
|
if want == "" || want == "containers" {
|
|
want = fmt.Sprintf("gid %d", g.GID)
|
|
}
|
|
t.Run(fmt.Sprintf("%d %s", g.GID, g.Name), func(t *testing.T) {
|
|
shared := t.TempDir()
|
|
groupOfPaths(t, map[string]PathGroup{shared: g})
|
|
m := newGrantMachine("operator", nil, g.Name)
|
|
|
|
report, _, err := applyGrants(t, m, store.State{},
|
|
accessGranting("importer.shared", shared, "operator", "importer.app"), mounting("importer.app", shared))
|
|
if err == nil {
|
|
t.Fatal("a read-write access to a path of a root-granting group applied")
|
|
}
|
|
var failed *Error
|
|
if !errors.As(err, &failed) || failed.Resource != "importer.shared" ||
|
|
!strings.Contains(err.Error(), want) || !strings.Contains(err.Error(), "never") {
|
|
t.Errorf("the refusal does not name the access and the group: %v", err)
|
|
}
|
|
if g.Name != "" && strings.Contains(m.groups(), g.Name) {
|
|
t.Errorf("the account was put in %s", g.Name)
|
|
}
|
|
if run := strings.Join(m.runs["importer-app"], " "); strings.Contains(run, "--group-add") {
|
|
t.Errorf("the container was given a root-granting group: %s", run)
|
|
}
|
|
_ = report
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAGrantedGroupIsTakenBackUnlessStillAskedForOrHeldBefore(t *testing.T) {
|
|
music, movies := t.TempDir(), t.TempDir()
|
|
groupOfPaths(t, map[string]PathGroup{music: {GID: 1001, Name: "media", Writable: true},
|
|
movies: {GID: 1001, Name: "media", Writable: true}})
|
|
|
|
t.Run("no longer asked", func(t *testing.T) {
|
|
m := newGrantMachine("operator", []string{"operator"}, "media")
|
|
_, state, err := applyGrants(t, m, store.State{}, accessGranting("importer.music", music, "operator"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
report, state, err := applyGrants(t, m, state, mounting("other.app", t.TempDir()))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(m.groups(), "media") {
|
|
t.Errorf("the group the mesh granted was not taken back: %q", m.groups())
|
|
}
|
|
if o := outcomeOf(report, "importer.music"); o.Action != "restored" || !strings.Contains(o.Detail, "taken out of media") {
|
|
t.Errorf("the removal did not say it took the group back: %+v", o)
|
|
}
|
|
if _, still := state.Find("importer.music"); still {
|
|
t.Error("the access is still recorded")
|
|
}
|
|
})
|
|
|
|
t.Run("still asked by another access", func(t *testing.T) {
|
|
m := newGrantMachine("operator", []string{"operator"}, "media")
|
|
_, state, err := applyGrants(t, m, store.State{}, accessGranting("importer.music", music, "operator"),
|
|
accessGranting("sonarr.movies", movies, "operator"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
report, _, err := applyGrants(t, m, state, accessGranting("sonarr.movies", movies, "operator"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(m.groups(), "media") {
|
|
t.Errorf("a group another access still grants was taken back: %q", m.groups())
|
|
}
|
|
if o := outcomeOf(report, "importer.music"); !strings.Contains(o.Detail, "still asks for") {
|
|
t.Errorf("the removal did not say why the group stayed: %+v", o)
|
|
}
|
|
})
|
|
|
|
t.Run("still asked by a user resource", func(t *testing.T) {
|
|
m := newGrantMachine("operator", []string{"operator"}, "media")
|
|
_, state, err := applyGrants(t, m, store.State{}, accessGranting("importer.music", music, "operator"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := applyGrants(t, m, state,
|
|
`{"id":"player.account","type":"user","name":"operator","groups":["media"]}`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(m.groups(), "media") {
|
|
t.Errorf("a group a user resource asks for was taken back: %q", m.groups())
|
|
}
|
|
})
|
|
|
|
t.Run("held before", func(t *testing.T) {
|
|
m := newGrantMachine("operator", []string{"operator", "media"})
|
|
_, state, err := applyGrants(t, m, store.State{}, accessGranting("importer.music", music, "operator"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec, _ := state.Find("importer.music"); len(rec.Groups) != 0 {
|
|
t.Fatalf("a group the account held before was recorded as the mesh's: %+v", rec)
|
|
}
|
|
if _, _, err := applyGrants(t, m, state, mounting("other.app", t.TempDir())); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(m.groups(), "media") {
|
|
t.Errorf("a group the account held before the mesh was taken back: %q", m.groups())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAContainerIsRecreatedWithTheGroupItIsGranted(t *testing.T) {
|
|
library := t.TempDir()
|
|
groupOfPaths(t, map[string]PathGroup{library: {GID: 1001, Name: "media", Writable: true}})
|
|
m := newGrantMachine("operator", nil, "media")
|
|
|
|
// Running before its access granted anything: a controller older than the grant.
|
|
_, state, err := applyGrants(t, m, store.State{},
|
|
`{"id":"importer.music","type":"access","path":"`+library+`","mode":"read-write"}`, mounting("importer.app", library))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if run := strings.Join(m.runs["importer-app"], " "); strings.Contains(run, "--group-add") {
|
|
t.Fatalf("a container granted nothing was given a group: %s", run)
|
|
}
|
|
report, _, err := applyGrants(t, m, state,
|
|
accessGranting("importer.music", library, "", "importer.app"), mounting("importer.app", library))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
o := outcomeOf(report, "importer.app")
|
|
if o.Action != "updated" || !strings.Contains(o.Detail, "recreated to hold the groups") {
|
|
t.Errorf("the container was not recreated for its group: %+v", o)
|
|
}
|
|
if run := strings.Join(m.runs["importer-app"], " "); !strings.Contains(run, "--group-add 1001") {
|
|
t.Errorf("the recreated container does not hold the group: %s", run)
|
|
}
|
|
}
|
|
|
|
func TestTheToolRunnerIsRestartedOnceItsAccountHoldsTheGroup(t *testing.T) {
|
|
me, err := user.Current()
|
|
if err != nil {
|
|
t.Skip("no current account to run the tool runner as")
|
|
}
|
|
units, bundles := t.TempDir(), t.TempDir()
|
|
wasUnits, wasBundles := unitDir, daemonRoot
|
|
unitDir, daemonRoot = units, bundles
|
|
t.Cleanup(func() { unitDir, daemonRoot = wasUnits, wasBundles })
|
|
library := t.TempDir()
|
|
groupOfPaths(t, map[string]PathGroup{library: {GID: 1001, Name: "media", Writable: true}})
|
|
m := newGrantMachine(me.Username, nil, "media")
|
|
|
|
body, digest := anArchive(t, map[string]string{"main.js": "console.log(1)\n"})
|
|
runner := `{"id":"node-tools.runtime","type":"process","name":"node-tools","source":"` + serving(t, body) +
|
|
`","digest":"` + digest + `","run":["node","main.js"],"user":"` + me.Username +
|
|
`","restart-on":["importer.music"]}`
|
|
// Running before the grant.
|
|
_, state, err := applyGrants(t, m, store.State{}, runner)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m.restarts = nil
|
|
report, state, err := applyGrants(t, m, state, accessGranting("importer.music", library, me.Username), runner)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(m.restarts) != 1 || !strings.HasPrefix(m.restarts[0], "node-tools.service: ") ||
|
|
!strings.Contains(m.restarts[0], "media") {
|
|
t.Fatalf("the tool runner was not restarted once its account held the group: %v", m.restarts)
|
|
}
|
|
if o := outcomeOf(report, "node-tools.runtime"); !strings.Contains(o.Detail, "importer.music") {
|
|
t.Errorf("the restart does not say what it was for: %+v", o)
|
|
}
|
|
// And not again while nothing moves.
|
|
m.restarts = nil
|
|
if _, _, err := applyGrants(t, m, state, accessGranting("importer.music", library, me.Username), runner); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(m.restarts) != 0 {
|
|
t.Errorf("the tool runner was restarted with nothing moved: %v", m.restarts)
|
|
}
|
|
}
|
|
|
|
// sessionReader is an account whose own service manager runs, holding the groups it began with.
|
|
type sessionReader struct{ database, session []string }
|
|
|
|
func (s sessionReader) InDatabase(context.Context, string) ([]string, error) { return s.database, nil }
|
|
func (s sessionReader) Session(_ context.Context, _ string, groups []string) (accounts.Session, error) {
|
|
has := map[string]bool{}
|
|
for _, g := range s.session {
|
|
has[g] = true
|
|
}
|
|
return accounts.Session{Running: true, Has: has}, nil
|
|
}
|
|
func (s sessionReader) Escalation(context.Context, string, []string) ([]string, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func TestAnAccountsOwnSessionIsSaidReloginNeededForAGrantedGroup(t *testing.T) {
|
|
library := t.TempDir()
|
|
groupOfPaths(t, map[string]PathGroup{library: {GID: 1001, Name: "media", Writable: true}})
|
|
d := parse(t, `{"declaration":1,"resources":[`+accessGranting("importer.music", library, "operator")+`]}`)
|
|
|
|
as := GrantedAccounts(d, nil)
|
|
if len(as) != 1 || as[0].Module != "importer" || as[0].Name != "operator" || strings.Join(as[0].Groups, " ") != "media" {
|
|
t.Fatalf("the account the access grants is not judged: %+v", as)
|
|
}
|
|
j := accounts.New(sessionReader{database: []string{"operator", "media"}, session: []string{"operator"}})
|
|
j.Set(as)
|
|
st, _ := j.Look(context.Background())
|
|
if len(st.Accounts) != 1 || !strings.HasPrefix(st.Accounts[0].Reason, accounts.ReasonRelogin) {
|
|
t.Errorf("a session of the account's own manager that began before the grant is not said relogin needed: %+v",
|
|
st.Accounts)
|
|
}
|
|
// A read access, or one refused, asks nothing of the judge.
|
|
groupOfPaths(t, map[string]PathGroup{library: {GID: 0, Name: "root", Writable: true}})
|
|
if as := GrantedAccounts(d, nil); len(as) != 0 {
|
|
t.Errorf("a refused grant is judged: %+v", as)
|
|
}
|
|
}
|
|
|
|
// An access going while a user resource still asks for its group hands the group over: the user resource records it
|
|
// as the mesh's, and takes it back when it stops asking too (novox/hq ADR 0312 §5, ADR 0252 §3).
|
|
func TestAGroupStillAskedForIsHandedOverAndTakenBackWhenTheLastAskerGoes(t *testing.T) {
|
|
music := t.TempDir()
|
|
groupOfPaths(t, map[string]PathGroup{music: {GID: 1001, Name: "media", Writable: true}})
|
|
player := `{"id":"player.account","type":"user","name":"operator","groups":["media"]}`
|
|
m := newGrantMachine("operator", []string{"operator"}, "media")
|
|
|
|
_, state, err := applyGrants(t, m, store.State{}, accessGranting("importer.music", music, "operator"), player)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec, _ := state.Find("player.account"); len(rec.Groups) != 0 {
|
|
t.Fatalf("the user resource recorded a group the access put the account in: %+v", rec)
|
|
}
|
|
_, state, err = applyGrants(t, m, state, player)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(m.groups(), "media") {
|
|
t.Fatalf("a group the user resource still asks for was taken back: %q", m.groups())
|
|
}
|
|
if rec, _ := state.Find("player.account"); strings.Join(rec.Groups, " ") != "media" {
|
|
t.Fatalf("the group was not handed to the user resource that still asks for it: %+v", rec)
|
|
}
|
|
if _, _, err := applyGrants(t, m, state, mounting("other.app", t.TempDir())); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(m.groups(), "media") {
|
|
t.Errorf("the group stayed after the last resource asking for it went: %q", m.groups())
|
|
}
|
|
}
|
|
|
|
// A grant's account is an argument to id, usermod and gpasswd, and a name that starts with a dash would be read as
|
|
// an option by each.
|
|
func TestAGrantsAccountMayNotReadAsAnOption(t *testing.T) {
|
|
for _, name := range []string{"-ogroups", "--help", "a b", "a:b"} {
|
|
_, err := declaration.Parse([]byte(`{"declaration":1,"resources":[{"id":"m.lib","type":"access","path":"/srv/lib",` +
|
|
`"mode":"read-write","grant":{"account":"` + name + `"}}]}`))
|
|
if err == nil || !strings.Contains(err.Error(), "not starting with a dash") {
|
|
t.Errorf("the account %q was accepted: %v", name, err)
|
|
}
|
|
}
|
|
if _, err := declaration.Parse([]byte(`{"declaration":1,"resources":[{"id":"m.lib","type":"access","path":"/srv/lib",` +
|
|
`"mode":"read-write","grant":{"account":"jochen"}}]}`)); err != nil {
|
|
t.Errorf("an ordinary account was refused: %v", err)
|
|
}
|
|
}
|
|
|
|
// The scheduler reads no path issue 339's guard refuses, as the apply reads none.
|
|
func TestAScheduledStepIsArmedWithoutReadingARefusedPath(t *testing.T) {
|
|
var read []string
|
|
was := GroupOfPath
|
|
GroupOfPath = func(path string) (PathGroup, error) {
|
|
read = append(read, path)
|
|
return PathGroup{GID: 1001, Name: "media", Writable: true}, nil
|
|
}
|
|
t.Cleanup(func() { GroupOfPath = was })
|
|
rootGroupsAre(t, map[int]string{0: "root"})
|
|
d := parse(t, `{"declaration":1,"resources":[`+accessGranting("m.etc", "/etc", "", "m.step")+`,`+
|
|
`{"id":"m.step","type":"container","name":"step","image":"`+pinned+`","schedule":"0 3 * * *","volumes":["/etc:/x"]}]}`)
|
|
if got := ContainerGroupsOf(d); len(got["m.step"]) != 0 {
|
|
t.Errorf("a step was armed with the group of a refused path: %v", got)
|
|
}
|
|
if len(read) != 0 {
|
|
t.Errorf("the scheduler read a path the guard refuses: %v", read)
|
|
}
|
|
}
|
|
|
|
// The group database is read as name to number, so a number carrying a root group's name under any other name
|
|
// is still that number.
|
|
func TestGroupEntriesAreReadByNumber(t *testing.T) {
|
|
got := groupEntries("root:x:0:\n# a comment\ndocker:x:968:operator\ncontainers:x:968:\nbroken\n")
|
|
if got["root"] != 0 || got["docker"] != 968 || got["containers"] != 968 || len(got) != 3 {
|
|
t.Errorf("read %v", got)
|
|
}
|
|
}
|