services/mgmt/device/impl: per-instance ACLs control logs access
This change extends per-instance ACLs to provide access control to
logs under the __debug space of a device manager invoked application
and adds additional testing to demonstrate globbing and access control
to the entire __debug space.
Change-Id: I8071fa470bd9c001620c3a1e995d0c2e8e5c8963
diff --git a/services/mgmt/device/impl/app_service.go b/services/mgmt/device/impl/app_service.go
index 0087d16..8abc3e9 100644
--- a/services/mgmt/device/impl/app_service.go
+++ b/services/mgmt/device/impl/app_service.go
@@ -662,6 +662,7 @@
return installFrom(overridePackages, installationDir)
}
+// initializeSubAccessLists updates the provided acl for instance-specific ACLs
func (i *appService) initializeSubAccessLists(instanceDir string, blessings []string, acl access.Permissions) error {
for _, b := range blessings {
for _, tag := range access.AllTypicalTags() {
diff --git a/services/mgmt/device/impl/debug_acls_test.go b/services/mgmt/device/impl/debug_acls_test.go
index 56a167d..26136d6 100644
--- a/services/mgmt/device/impl/debug_acls_test.go
+++ b/services/mgmt/device/impl/debug_acls_test.go
@@ -8,7 +8,7 @@
"v.io/v23/naming"
"v.io/v23/security"
"v.io/v23/services/security/access"
- "v.io/v23/vdl"
+ "v.io/v23/verror"
mgmttest "v.io/x/ref/services/mgmt/lib/testutil"
"v.io/x/ref/test/testutil"
@@ -17,15 +17,29 @@
func updateAccessList(t *testing.T, ctx *context.T, blessing, right string, name ...string) {
acl, etag, err := appStub(name...).GetPermissions(ctx)
if err != nil {
- t.Fatalf("GetPermissions(%v) failed %v", name, err)
+ t.Fatalf(testutil.FormatLogLine(2, "GetACL(%v) failed %v", name, err))
}
acl.Add(security.BlessingPattern(blessing), right)
if err = appStub(name...).SetPermissions(ctx, acl, etag); err != nil {
- t.Fatalf("SetPermissions(%v, %v, %v) failed: %v", name, blessing, right, err)
+ t.Fatalf(testutil.FormatLogLine(2, "SetPermissions(%v, %v, %v) failed: %v", name, blessing, right, err))
}
}
-func TestDebugAccessListPropagation(t *testing.T) {
+func mn(base []string, suffix ...string) []string {
+ fn := make([]string, len(base))
+ // Must copy because append will modify the original.
+ copy(fn, base)
+ fn = append(fn, suffix...)
+ return fn
+}
+
+func testAccessFail(t *testing.T, expected verror.ID, ctx *context.T, who string, name ...string) {
+ if _, err := statsStub(mn(name, "stats/system/pid")...).Value(ctx); !verror.Is(err, expected) {
+ t.Fatalf(testutil.FormatLogLine(2, "%s got error %v but expected %v", who, err, expected))
+ }
+}
+
+func TestDebugPermissionsPropagation(t *testing.T) {
cleanup, ctx, sh, envelope, root, helperPath, idp := startupHelper(t)
defer cleanup()
@@ -63,58 +77,69 @@
// Bob permits Alice to read from his app.
updateAccessList(t, bobCtx, "root/alice/$", string(access.Read), appID, bobApp)
- // Confirm that self can access stats name (i.e. apps proxied from
- // the __debug space of the app.
- // TODO(rjkroege): validate each of the services under __debug.
- v, err := statsStub(appID, bobApp, "stats/system/pid").Value(ctx)
- if err != nil {
- t.Fatalf("Value() failed: %v\n", err)
+ // Create some globbing test vectors.
+ globtests := []globTestVector{
+ {naming.Join("dm", "apps", appID, bobApp), "*",
+ []string{"logs", "pprof", "stats"},
+ },
+ {naming.Join("dm", "apps", appID, bobApp, "stats", "system"),
+ "start-time*",
+ []string{"start-time-rfc1123", "start-time-unix"},
+ },
+ {naming.Join("dm", "apps", appID, bobApp, "logs"),
+ "*",
+ []string{
+ "STDERR-<timestamp>",
+ "STDOUT-<timestamp>",
+ "app.INFO",
+ "app.<*>.INFO.<timestamp>",
+ },
+ },
}
- var pid int
- if err := vdl.Convert(&pid, v); err != nil {
- t.Fatalf("pid returned from stats interface is not an int: %v", err)
- }
+ globtestminus := globtests[1:]
+ res := newGlobTestRegexHelper("app")
+
+ // Confirm that self can access __debug names.
+ verifyGlob(t, selfCtx, "app", globtests, res)
+ verifyStatsValues(t, selfCtx, "dm", "apps", appID, bobApp, "stats/system/start-time*")
+ verifyLog(t, selfCtx, "dm", "apps", appID, bobApp, "logs", "*")
+ verifyPProfCmdLine(t, selfCtx, "app", "dm", "apps", appID, bobApp, "pprof")
// Bob has an issue with his app and tries to use the debug output to figure it out.
- if _, err = statsStub(appID, bobApp, "stats/system/pid").Value(bobCtx); err != nil {
- t.Fatalf("Bob couldn't access debug info on his app: ", err)
- }
+ verifyGlob(t, bobCtx, "app", globtests, res)
+ verifyStatsValues(t, bobCtx, "dm", "apps", appID, bobApp, "stats/system/start-time*")
+ verifyLog(t, bobCtx, "dm", "apps", appID, bobApp, "logs", "*")
+ verifyPProfCmdLine(t, bobCtx, "app", "dm", "apps", appID, bobApp, "pprof")
// But Bob can't figure it out and hopes that hackerjoe can debug it.
updateAccessList(t, bobCtx, "root/hackerjoe/$", string(access.Debug), appID, bobApp)
// Fortunately the device manager permits hackerjoe to access the stats.
// But hackerjoe can't solve Bob's problem.
- if _, err = statsStub(appID, bobApp, "stats/system/pid").Value(hjCtx); err != nil {
- t.Fatalf("hackerjoe couldn't access debug info on the app: %v", err)
- }
-
- // Show that hackerjoe can glob the debug space.
- results, _, err := testutil.GlobName(hjCtx, naming.Join("dm/apps", appID, bobApp, "stats"), "...")
- if err != nil {
- t.Fatalf("Debug rights should let hackerjoe glob the __debug space: %v", err)
- }
- if len(results) == 0 {
- t.Fatalf("hackerjoe didn't successfully glob the __debug space: no matches returned.")
- }
+ // Because hackerjob has Debug, hackerjoe can glob the __debug resources
+ // of Bob's app but can't glob Bob's app.
+ verifyGlob(t, hjCtx, "app", globtestminus, res)
+ verifyFailGlob(t, hjCtx, "app", globtests[0:1], res)
+ verifyStatsValues(t, hjCtx, "dm", "apps", appID, bobApp, "stats", "system/start-time*")
+ verifyLog(t, hjCtx, "dm", "apps", appID, bobApp, "logs", "*")
+ verifyPProfCmdLine(t, hjCtx, "app", "dm", "apps", appID, bobApp, "pprof")
// Alice might be able to help but Bob didn't give Alice access to the debug ACLs.
- if _, err = statsStub(appID, bobApp, "stats/system/pid").Value(aliceCtx); err == nil {
- t.Fatalf("Alice could wrongly access the stats without perms.")
- }
+ testAccessFail(t, verror.ErrNoAccess.ID, aliceCtx, "Alice", appID, bobApp)
// Bob forgets that Alice can't read the stats when he can.
- if _, err = statsStub(appID, bobApp, "stats/system/pid").Value(bobCtx); err != nil {
- t.Fatalf("Bob couldn't access debug info on his app: ", err)
- }
+ verifyGlob(t, bobCtx, "app", globtests, res)
+ verifyStatsValues(t, bobCtx, "dm", "apps", appID, bobApp, "stats/system/start-time*")
// So Bob changes the permissions so that Alice can help debug too.
updateAccessList(t, bobCtx, "root/alice/$", string(access.Debug), appID, bobApp)
// Alice can access __debug content.
- if _, err = statsStub(appID, bobApp, "stats/system/pid").Value(aliceCtx); err != nil {
- t.Fatalf("Alice couldn't access debug info on the app: %v", err)
- }
+ verifyGlob(t, aliceCtx, "app", globtestminus, res)
+ verifyFailGlob(t, aliceCtx, "app", globtests[0:1], res)
+ verifyStatsValues(t, aliceCtx, "dm", "apps", appID, bobApp, "stats", "system/start-time*")
+ verifyLog(t, aliceCtx, "dm", "apps", appID, bobApp, "logs", "*")
+ verifyPProfCmdLine(t, aliceCtx, "app", "dm", "apps", appID, bobApp, "pprof")
// Bob is glum because no one can help him fix his app so he stops it.
stopApp(t, bobCtx, appID, bobApp)
diff --git a/services/mgmt/device/impl/dispatcher.go b/services/mgmt/device/impl/dispatcher.go
index b4e6e91..da1e4e3 100644
--- a/services/mgmt/device/impl/dispatcher.go
+++ b/services/mgmt/device/impl/dispatcher.go
@@ -282,7 +282,11 @@
case "logs":
logsDir := filepath.Join(appInstanceDir, "logs")
suffix := naming.Join(components[5:]...)
- return logsimpl.NewLogFileService(logsDir, suffix), auth, nil
+ appSpecificAuthorizer, err := newAppSpecificAuthorizer(auth, d.config, components[1:], d.aclstore)
+ if err != nil {
+ return nil, nil, err
+ }
+ return logsimpl.NewLogFileService(logsDir, suffix), appSpecificAuthorizer, nil
case "pprof", "stats":
info, err := loadInstanceInfo(nil, appInstanceDir)
if err != nil {
diff --git a/services/mgmt/device/impl/util_test.go b/services/mgmt/device/impl/util_test.go
index e6aba11..de3d380 100644
--- a/services/mgmt/device/impl/util_test.go
+++ b/services/mgmt/device/impl/util_test.go
@@ -505,12 +505,25 @@
}
}
+// verifyFailGlob verifies that for each globTestVector instance that the
+// pattern returns no matches.
+func verifyFailGlob(t *testing.T, ctx *context.T, appName string, testcases []globTestVector, res *globTestRegexHelper) {
+ for _, tc := range testcases {
+ results, _, _ := testutil.GlobName(ctx, tc.name, tc.pattern)
+ if len(results) != 0 {
+ t.Errorf(testutil.FormatLogLine(2, "verifyFailGlob should have failed for %q, %q", tc.name, tc.pattern))
+ }
+ }
+}
+
// verifyLog calls Size() on a selection of log file objects to
// demonstrate that the log files are accessible and have been written by
// the application.
-func verifyLog(t *testing.T, ctx *context.T, service string, nameComponents ...string) {
- path := naming.Join(nameComponents...)
- files, _, err := testutil.GlobName(ctx, service, path)
+func verifyLog(t *testing.T, ctx *context.T, nameComponents ...string) {
+ a := nameComponents
+ pattern, prefix := a[len(a)-1], a[:len(a)-1]
+ path := naming.Join(prefix...)
+ files, _, err := testutil.GlobName(ctx, path, pattern)
if err != nil {
t.Errorf(testutil.FormatLogLine(2, "unexpected glob error: %v", err))
}
@@ -518,7 +531,7 @@
t.Errorf(testutil.FormatLogLine(2, "Unexpected number of matches. Got %d, want at least %d", got, want))
}
for _, file := range files {
- name := naming.Join("dm", file)
+ name := naming.Join(path, file)
c := logreader.LogFileClient(name)
if _, err := c.Size(ctx); err != nil {
t.Errorf(testutil.FormatLogLine(2, "Size(%q) failed: %v", name, err))
@@ -528,9 +541,12 @@
// verifyStatsValues call Value() on some of the stats objects to prove
// that they are correctly being proxied to the device manager.
-func verifyStatsValues(t *testing.T, ctx *context.T, service string, nameComponents ...string) {
- path := naming.Join(nameComponents...)
- objects, _, err := testutil.GlobName(ctx, service, path)
+func verifyStatsValues(t *testing.T, ctx *context.T, nameComponents ...string) {
+ a := nameComponents
+ pattern, prefix := a[len(a)-1], a[:len(a)-1]
+ path := naming.Join(prefix...)
+ objects, _, err := testutil.GlobName(ctx, path, pattern)
+
if err != nil {
t.Errorf(testutil.FormatLogLine(2, "unexpected glob error: %v", err))
}
@@ -538,7 +554,7 @@
t.Errorf(testutil.FormatLogLine(2, "Unexpected number of matches. Got %d, want %d", got, want))
}
for _, obj := range objects {
- name := naming.Join("dm", obj)
+ name := naming.Join(path, obj)
c := stats.StatsClient(name)
if _, err := c.Value(ctx); err != nil {
t.Errorf(testutil.FormatLogLine(2, "Value(%q) failed: %v", name, err))