blob: 260ebf075eac73c66c1f7ef1ab64a3e649075338 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package agentlib_test
import (
"bytes"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"text/template"
"v.io/v23/security"
"v.io/x/ref"
vsecurity "v.io/x/ref/lib/security"
"v.io/x/ref/lib/v23test"
_ "v.io/x/ref/runtime/factories/generic"
"v.io/x/ref/services/agent/agentlib"
"v.io/x/ref/services/agent/keymgr"
)
func start(c *v23test.Cmd) {
c.S.SetVerbosity(true)
c.Start()
}
func TestV23PassPhraseUse(t *testing.T) {
v23test.SkipUnlessRunningIntegrationTests(t)
sh := v23test.NewShell(t, v23test.Opts{})
defer sh.Cleanup()
bin := sh.BuildGoPkg("v.io/x/ref/services/agent/agentd")
dir := sh.MakeTempDir()
// Create the passphrase
c := sh.Cmd(bin, "echo", "Hello")
c.Vars[ref.EnvCredentials] = dir
c.Stdin = "PASSWORD"
start(c)
c.S.ReadLine() // Skip over ...creating new key... message
c.S.Expect("Hello")
c.Wait()
// Use it successfully
c = sh.Cmd(bin, "echo", "Hello")
c.Vars[ref.EnvCredentials] = dir
c.Stdin = "PASSWORD"
start(c)
c.S.Expect("Hello")
c.Wait()
// Provide a bad password
c = sh.Cmd(bin, "echo", "Hello")
c.Vars[ref.EnvCredentials] = dir
c.Stdin = "BADPASSWORD"
c.ExitErrorIsOk = true
stdout, stderr := c.StdoutStderr()
if c.Err == nil {
t.Fatalf("expected an error.STDOUT:%v\nSTDERR:%v\n", stdout, stderr)
}
if got, want := c.Err.Error(), "exit status 1"; got != want {
t.Errorf("Got %q, want %q", got, want)
}
if got, want := stderr, "passphrase incorrect for decrypting private key"; !strings.Contains(got, want) {
t.Errorf("Got %q, wanted it to contain %q", got, want)
}
}
func TestV23AllPrincipalMethods(t *testing.T) {
v23test.SkipUnlessRunningIntegrationTests(t)
sh := v23test.NewShell(t, v23test.Opts{})
defer sh.Cleanup()
// Test all methods of the principal interface.
// (Errors are printed to STDERR)
testbin := sh.BuildGoPkg("v.io/x/ref/services/agent/internal/test_principal")
agentd := sh.BuildGoPkg("v.io/x/ref/services/agent/agentd")
c := sh.Cmd(agentd, testbin)
c.Vars[ref.EnvCredentials] = sh.MakeTempDir()
c.Run()
}
func TestV23AgentProcesses(t *testing.T) {
v23test.SkipUnlessRunningIntegrationTests(t)
sh := v23test.NewShell(t, v23test.Opts{})
defer sh.Cleanup()
// Setup two principals: One for the agent that runs the pingpong
// server, one for the client. Since the server uses the default
// authorization policy, the client must have a blessing delegated from
// the server.
var (
clientDir, serverDir = createClientAndServerCredentials(t, sh)
pingpong = sh.BuildGoPkg("v.io/x/ref/services/agent/internal/pingpong")
agentd = sh.BuildGoPkg("v.io/x/ref/services/agent/agentd")
)
server := sh.Cmd(agentd, pingpong)
server.Vars[ref.EnvCredentials] = serverDir
start(server)
serverName := server.S.ExpectVar("NAME")
// Run the client via an agent once.
client := sh.Cmd(agentd, pingpong, serverName)
client.Vars[ref.EnvCredentials] = clientDir
start(client)
client.S.Expect("Pinging...")
client.S.Expect("pong (client:[pingpongd:client] server:[pingpongd])")
client.Wait()
// Run it through a shell to test that the agent can pass credentials
// to subprocess of a shell (making things like "agentd bash" provide
// useful terminals).
// This only works with shells that propagate open file descriptors to
// children. POSIX-compliant shells do this as to many other commonly
// used ones like bash.
script := filepath.Join(sh.MakeTempDir(), "test.sh")
if err := writeScript(
script,
`#!/bin/bash
echo "Running client"
{{.Bin}} {{.Server}} || exit 101
echo "Running client again"
{{.Bin}} {{.Server}} || exit 102
`,
struct{ Bin, Server string }{pingpong, serverName}); err != nil {
t.Fatal(err)
}
client = sh.Cmd(agentd, "bash", script)
client.Vars[ref.EnvCredentials] = clientDir
start(client)
client.S.Expect("Running client")
client.S.Expect("Pinging...")
client.S.Expect("pong (client:[pingpongd:client] server:[pingpongd])")
client.S.Expect("Running client again")
client.S.Expect("Pinging...")
client.S.Expect("pong (client:[pingpongd:client] server:[pingpongd])")
client.Wait()
}
func TestV23AgentRestartExitCode(t *testing.T) {
v23test.SkipUnlessRunningIntegrationTests(t)
sh := v23test.NewShell(t, v23test.Opts{})
defer sh.Cleanup()
var (
clientDir, serverDir = createClientAndServerCredentials(t, sh)
pingpong = sh.BuildGoPkg("v.io/x/ref/services/agent/internal/pingpong")
agentd = sh.BuildGoPkg("v.io/x/ref/services/agent/agentd")
scriptDir = sh.MakeTempDir()
counter = filepath.Join(scriptDir, "counter")
script = filepath.Join(scriptDir, "test.sh")
)
server := sh.Cmd(agentd, pingpong)
server.Vars[ref.EnvCredentials] = serverDir
start(server)
serverName := server.S.ExpectVar("NAME")
if err := writeScript(
script,
`#!/bin/bash
# Execute the client ping once
{{.Bin}} {{.Server}} || exit 101
# Increment the contents of the counter file
readonly COUNT=$(expr $(<"{{.Counter}}") + 1)
echo -n $COUNT >{{.Counter}}
# Exit code is 0 if the counter is less than 5
[[ $COUNT -lt 5 ]]; exit $?
`,
struct{ Bin, Server, Counter string }{
Bin: pingpong,
Server: serverName,
Counter: counter,
}); err != nil {
t.Fatal(err)
}
tests := []struct {
RestartExitCode string
WantError string
WantCounter string
}{
{
// With --restart-exit-code=0, the script should be kicked off
// 5 times till the counter reaches 5 and the last iteration of
// the script will exit.
RestartExitCode: "0",
WantError: "exit status 1",
WantCounter: "5",
},
{
// With --restart-exit-code=!0, the script will be executed only once.
RestartExitCode: "!0",
WantError: "",
WantCounter: "1",
},
{
// --restart-exit-code=!1, should be the same
// as --restart-exit-code=0 for this
// particular script only exits with 0 or 1
RestartExitCode: "!1",
WantError: "exit status 1",
WantCounter: "5",
},
}
for _, test := range tests {
// Clear out the counter file.
if err := ioutil.WriteFile(counter, []byte("0"), 0644); err != nil {
t.Fatalf("%q: %v", counter, err)
}
// Run the script under the agent
cmd := sh.Cmd(agentd, "--restart-exit-code="+test.RestartExitCode, "bash", "-c", script)
cmd.Vars[ref.EnvCredentials] = clientDir
cmd.ExitErrorIsOk = true
cmd.Run()
var gotError string
if cmd.Err != nil {
gotError = cmd.Err.Error()
}
if got, want := gotError, test.WantError; got != want {
t.Errorf("%+v: Got %q, want %q", test, got, want)
}
if buf, err := ioutil.ReadFile(counter); err != nil {
t.Errorf("ioutil.ReadFile(%q) failed: %v", counter, err)
} else if got, want := string(buf), test.WantCounter; got != want {
t.Errorf("%+v: Got %q, want %q", test, got, want)
}
}
}
func TestV23KeyManager(t *testing.T) {
v23test.SkipUnlessRunningIntegrationTests(t)
sh := v23test.NewShell(t, v23test.Opts{})
defer sh.Cleanup()
// Start up an agent that serves additional principals.
// Since the agent needs to run some long-lived program to stay up,
// use a script that waits for user input.
var (
agentDir, otherDir = sh.MakeTempDir(), sh.MakeTempDir()
agentSock = filepath.Join(agentDir, "agent.sock") // "agent.sock" comes from agentd/main.go
agentd = sh.BuildGoPkg("v.io/x/ref/services/agent/agentd")
clientSock = filepath.Join(sh.MakeTempDir(), "client.sock")
script = filepath.Join(sh.MakeTempDir(), "test.sh")
startAgent = func() (*v23test.Cmd, io.WriteCloser) {
agent := sh.Cmd(agentd, "--additional-principals="+otherDir, "--no-passphrase", script)
agent.Vars[ref.EnvCredentials] = agentDir
wc := agent.StdinPipe()
start(agent)
for lineno := 0; lineno < 10; lineno++ {
line := agent.S.ReadLine()
if line == "STARTED" {
return agent, wc
}
t.Logf("Ignoring line from agent process: %q", line)
}
t.Fatal("Agent did not start correctly?")
return nil, nil
}
testClient = func() error {
p, err := agentlib.NewAgentPrincipalX(clientSock)
if err != nil {
return err
}
defer p.Close()
_, err = p.BlessSelf("foo")
return err
}
)
if err := writeScript(
script,
`#!/bin/bash
echo STARTED
read
`,
nil); err != nil {
t.Fatal(err)
}
agent, stdinPipe := startAgent()
// Setup the principal for the other key
kmgr, err := keymgr.NewKeyManager(agentSock)
if err != nil {
t.Fatal(err)
}
handle, err := kmgr.NewPrincipal(false)
if err != nil {
t.Fatal(err)
}
if err := kmgr.ServePrincipal(handle, clientSock); err != nil {
t.Fatal(err)
}
// clientSock should be functional.
if err := testClient(); err != nil {
t.Fatal(err)
}
// If we restart the agent, and have it serve the client principal on the same
// socket, the client should function again.
stdinPipe.Close()
agent.Shutdown(os.Interrupt)
t.Logf("Killed the agent, restarting it")
agent, stdinPipe = startAgent()
defer func() {
stdinPipe.Close()
agent.Shutdown(os.Interrupt)
}()
// The bug that this test is trying to fix is one where the clientSock
// file is left hanging around. At the time this test was written,
// the author couldn't figure out a nice way to "uncleanly" kill the agent
// and thus cause this problem. Anyway, ensure that this test is testing
// the issue it was intended to by checking that clientSock is still lying
// around.
if _, err := os.Stat(clientSock); err != nil {
t.Fatal(err)
}
// TODO(ashankar,ribrdb): To make clients resilient to the server
// listening on the unix socket restarting, should ipc.IPCConn or the
// KeyManager transparently retry when the connection has died instead
// of failing?
if kmgr, err = keymgr.NewKeyManager(agentSock); err != nil {
t.Fatal(err)
}
if err := kmgr.ServePrincipal(handle, clientSock); err != nil {
t.Fatal(err)
}
if err := testClient(); err != nil {
t.Fatal(err)
}
}
func writeScript(dstfile, tmpl string, args interface{}) error {
t, err := template.New(dstfile).Parse(tmpl)
if err != nil {
return err
}
var buf bytes.Buffer
if err := t.Execute(&buf, args); err != nil {
return err
}
if err := ioutil.WriteFile(dstfile, buf.Bytes(), 0700); err != nil {
return err
}
return nil
}
// createClientAndServerCredentials creates two principals, sets up their
// blessings and returns the created credentials directories.
//
// The server will have a single blessing "pingpongd".
// The client will have a single blessing "pingpongd:client", blessed by the
// server.
//
// Nearly identical to createClientAndServerCredentials in
// v.io/x/ref/cmd/vrun/vrun_v23_test.go.
func createClientAndServerCredentials(t *testing.T, sh *v23test.Shell) (clientDir, serverDir string) {
clientDir = sh.MakeTempDir()
serverDir = sh.MakeTempDir()
pserver, err := vsecurity.CreatePersistentPrincipal(serverDir, nil)
if err != nil {
t.Fatal(err)
}
pclient, err := vsecurity.CreatePersistentPrincipal(clientDir, nil)
if err != nil {
t.Fatal(err)
}
// Server will only serve, not make any client requests, so only needs a
// default blessing.
bserver, err := pserver.BlessSelf("pingpongd")
if err != nil {
t.Fatal(err)
}
if err := pserver.BlessingStore().SetDefault(bserver); err != nil {
t.Fatal(err)
}
// Clients need not have a default blessing as they will only make a request
// to the server.
bclient, err := pserver.Bless(pclient.PublicKey(), bserver, "client", security.UnconstrainedUse())
if err != nil {
t.Fatal(err)
}
if _, err := pclient.BlessingStore().Set(bclient, "pingpongd"); err != nil {
t.Fatal(err)
}
// The client and server must both recognize bserver and its delegates.
if err := security.AddToRoots(pserver, bserver); err != nil {
t.Fatal(err)
}
if err := security.AddToRoots(pclient, bserver); err != nil {
t.Fatal(err)
}
return
}