A user, bytes, and an archive — because most of what people install is
not a service A shell, a terminal, a chat client, a desktop are a package plus configuration in somebody's home. A mesh with no notion of a user can own /etc and nothing anybody looks at, which is most of the reason to manage a machine at all. Three shapes, and the vocabulary test asserts the count precisely because widening it widens what a compromised control plane can express: user a login, its shell and its groups archive a set of files, fetched by digest and unpacked (file) gains `bytes` for what is not text, and `owner` `user` also makes "zsh is my login shell" declared state. chsh is a command, the link may not carry one, and a shell settable only by hand is a shell the mesh cannot manage. Groups are additive and never pruned — usermod without --append REPLACES them, which would silently remove every group that makes a login able to use the machine. A machine's own groups are not the mesh's to know about. The archive is the one place this host reaches out on its own; everywhere else it holds one outbound connection and fetches nothing. So it carries the discipline the bootstrap already uses for images: pinned by digest, and the digest checked before a single file is written. Two decisions in the unpacker worth naming: - an entry naming a path outside the archive is REFUSED, not sanitised. Rewriting it to land inside would put a file somewhere nobody asked for and report success. Found by the test: the first version quietly relocated it. - symlinks and device nodes are refused rather than skipped, or an archive that needed one arrives silently incomplete. A partial host does archives and refuses users: an archive needs a filesystem and a way to fetch; a user needs a user database it is allowed to write.
This commit is contained in:
@@ -280,3 +280,68 @@ func TestAndroidsUnreachableAppliersFailLoudly(t *testing.T) {
|
||||
t.Errorf("android's service applier did not report it as unsupported: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestALoginThatIsNotThereIsAnAnswerAndABrokenDatabaseIsNot(t *testing.T) {
|
||||
// The distinction this package takes trouble over everywhere else, applied to users. A user
|
||||
// database that cannot be read must not be reported as "no such user" — absence read as
|
||||
// fact is the fault the whole host exists to prevent.
|
||||
notFound := func(context.Context, string, ...string) (string, error) {
|
||||
return "", errors.New("exit status 2")
|
||||
}
|
||||
if _, exists, err := LookUpUser(context.Background(), notFound, "nobody"); err != nil {
|
||||
t.Fatalf("a missing user was reported as a failure: %v", err)
|
||||
} else if exists {
|
||||
t.Fatal("a missing user was reported as present")
|
||||
}
|
||||
|
||||
broken := func(context.Context, string, ...string) (string, error) {
|
||||
return "", errors.New("exit status 71: cannot read /etc/passwd")
|
||||
}
|
||||
if _, exists, err := LookUpUser(context.Background(), broken, "somebody"); err == nil {
|
||||
t.Fatal("a broken user database was reported as an answer")
|
||||
} else if exists {
|
||||
t.Fatal("a broken user database reported a user as present")
|
||||
}
|
||||
}
|
||||
|
||||
func TestALoginIsReadFromThePasswdEntry(t *testing.T) {
|
||||
answering := func(context.Context, string, ...string) (string, error) {
|
||||
return "worker:x:1001:1001:,,,:/home/worker:/usr/bin/zsh\n", nil
|
||||
}
|
||||
login, exists, err := LookUpUser(context.Background(), answering, "worker")
|
||||
if err != nil || !exists {
|
||||
t.Fatalf("exists=%v err=%v", exists, err)
|
||||
}
|
||||
if login.Home != "/home/worker" || login.Shell != "/usr/bin/zsh" {
|
||||
t.Fatalf("got %+v", login)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSomethingThatIsNotAPasswdEntryIsRefused(t *testing.T) {
|
||||
// Rather than read as a login with empty fields, which would have the host decide the shell
|
||||
// differs and set it on every apply for ever.
|
||||
nonsense := func(context.Context, string, ...string) (string, error) {
|
||||
return "who knows\n", nil
|
||||
}
|
||||
if _, _, err := LookUpUser(context.Background(), nonsense, "worker"); err == nil {
|
||||
t.Fatal("nonsense was read as a login")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPartialHostRefusesUsersAndAllowsArchives(t *testing.T) {
|
||||
// An archive needs a filesystem and a way to fetch; a user needs a user database this host is
|
||||
// allowed to write, which Android does not have.
|
||||
speaks := map[declaration.Type]bool{}
|
||||
for _, shape := range (android{}).Shapes() {
|
||||
speaks[shape] = true
|
||||
}
|
||||
if !speaks[declaration.TypeArchive] {
|
||||
t.Error("a partial host refuses archives, which need only a filesystem")
|
||||
}
|
||||
if speaks[declaration.TypeUser] {
|
||||
t.Error("a partial host claims to manage users")
|
||||
}
|
||||
if err := (android{}).CreateUser(context.Background(), nil, "a", "", ""); err == nil {
|
||||
t.Error("a partial host created a user")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user