services/mgmt/lib/acls: adjust hierarchicalAuthorizer inheritance
Modify hierarchicalAuthorizer to only inherit root permissions for
blessings with the Admin tag.
Change-Id: I2579a6bce142dfe7c2d96ee36c5f195b05440f8d
diff --git a/services/mgmt/binary/impl/acl_test.go b/services/mgmt/binary/impl/acl_test.go
index 54cb252..281a594 100644
--- a/services/mgmt/binary/impl/acl_test.go
+++ b/services/mgmt/binary/impl/acl_test.go
@@ -65,6 +65,10 @@
return nil
}
+func b(name string) repository.BinaryClientStub {
+ return repository.BinaryClient(name)
+}
+
func TestBinaryCreateACL(t *testing.T) {
ctx, shutdown := testutil.InitForTest()
defer shutdown()
@@ -98,17 +102,16 @@
defer syscall.Kill(pid, syscall.SIGINT)
vlog.VI(2).Infof("Self uploads a shared and private binary.")
- binary := repository.BinaryClient("bini/private")
- if err := binary.Create(childCtx, 1, repository.MediaInfo{Type: "application/octet-stream"}); err != nil {
+ if err := b("bini/private").Create(childCtx, 1, repository.MediaInfo{Type: "application/octet-stream"}); err != nil {
t.Fatalf("Create() failed %v", err)
}
fakeDataPrivate := testData()
- if streamErr, err := invokeUpload(t, childCtx, binary, fakeDataPrivate, 0); streamErr != nil || err != nil {
+ if streamErr, err := invokeUpload(t, childCtx, b("bini/private"), fakeDataPrivate, 0); streamErr != nil || err != nil {
t.Fatalf("invokeUpload() failed %v, %v", err, streamErr)
}
vlog.VI(2).Infof("Validate that the ACL also allows Self")
- acl, _, err := binary.GetACL(selfCtx)
+ acl, _, err := b("bini/private").GetACL(selfCtx)
if err != nil {
t.Fatalf("GetACL failed: %v", err)
}
@@ -156,37 +159,32 @@
defer syscall.Kill(pid, syscall.SIGINT)
vlog.VI(2).Infof("Self uploads a shared and private binary.")
- binary := repository.BinaryClient("bini/private")
- if err := binary.Create(selfCtx, 1, repository.MediaInfo{Type: "application/octet-stream"}); err != nil {
+ if err := b("bini/private").Create(selfCtx, 1, repository.MediaInfo{Type: "application/octet-stream"}); err != nil {
t.Fatalf("Create() failed %v", err)
}
fakeDataPrivate := testData()
- if streamErr, err := invokeUpload(t, selfCtx, binary, fakeDataPrivate, 0); streamErr != nil || err != nil {
+ if streamErr, err := invokeUpload(t, selfCtx, b("bini/private"), fakeDataPrivate, 0); streamErr != nil || err != nil {
t.Fatalf("invokeUpload() failed %v, %v", err, streamErr)
}
- binary = repository.BinaryClient("bini/shared")
- if err := binary.Create(selfCtx, 1, repository.MediaInfo{Type: "application/octet-stream"}); err != nil {
+ if err := b("bini/shared").Create(selfCtx, 1, repository.MediaInfo{Type: "application/octet-stream"}); err != nil {
t.Fatalf("Create() failed %v", err)
}
fakeDataShared := testData()
- if streamErr, err := invokeUpload(t, selfCtx, binary, fakeDataShared, 0); streamErr != nil || err != nil {
+ if streamErr, err := invokeUpload(t, selfCtx, b("bini/shared"), fakeDataShared, 0); streamErr != nil || err != nil {
t.Fatalf("invokeUpload() failed %v, %v", err, streamErr)
}
vlog.VI(2).Infof("Verify that in the beginning other can't access bini/private or bini/shared")
- binary = repository.BinaryClient("bini/private")
- if _, _, err := binary.Stat(otherCtx); !verror.Is(err, verror.ErrNoAccess.ID) {
+ if _, _, err := b("bini/private").Stat(otherCtx); !verror.Is(err, verror.ErrNoAccess.ID) {
t.Fatalf("Stat() should have failed but didn't: %v", err)
}
- binary = repository.BinaryClient("bini/shared")
- if _, _, err := binary.Stat(otherCtx); !verror.Is(err, verror.ErrNoAccess.ID) {
+ if _, _, err := b("bini/shared").Stat(otherCtx); !verror.Is(err, verror.ErrNoAccess.ID) {
t.Fatalf("Stat() should have failed but didn't: %v", err)
}
vlog.VI(2).Infof("Validate the ACL file on bini/private.")
- binary = repository.BinaryClient("bini/private")
- acl, _, err := binary.GetACL(selfCtx)
+ acl, _, err := b("bini/private").GetACL(selfCtx)
if err != nil {
t.Fatalf("GetACL failed: %v", err)
}
@@ -202,8 +200,7 @@
}
vlog.VI(2).Infof("Validate the ACL file on bini/private.")
- binary = repository.BinaryClient("bini/private")
- acl, etag, err := binary.GetACL(selfCtx)
+ acl, etag, err := b("bini/private").GetACL(selfCtx)
if err != nil {
t.Fatalf("GetACL failed: %v", err)
}
@@ -226,12 +223,11 @@
acl.Clear("self", string(tag))
acl.Add("self/$", string(tag))
}
- if err := binary.SetACL(selfCtx, acl, etag); err != nil {
+ if err := b("bini/private").SetACL(selfCtx, acl, etag); err != nil {
t.Fatalf("SetACL failed: %v", err)
}
vlog.VI(2).Infof(" Verify that bini/private's acls are updated.")
- binary = repository.BinaryClient("bini/private")
updated := access.TaggedACLMap{
"Admin": access.ACL{In: []security.BlessingPattern{"self/$"}},
"Read": access.ACL{In: []security.BlessingPattern{"self/$"}},
@@ -239,7 +235,7 @@
"Debug": access.ACL{In: []security.BlessingPattern{"self/$"}},
"Resolve": access.ACL{In: []security.BlessingPattern{"self/$"}},
}
- acl, _, err = binary.GetACL(selfCtx)
+ acl, _, err = b("bini/private").GetACL(selfCtx)
if err != nil {
t.Fatalf("GetACL failed: %v", err)
}
@@ -251,50 +247,58 @@
// root level. Self has to set one explicitly to enable sharing. This way, self
// can't accidentally expose the server without setting a root ACL.
vlog.VI(2).Infof(" Verify that other still can't access bini/shared.")
- binary = repository.BinaryClient("bini/shared")
- if _, _, err := binary.Stat(otherCtx); !verror.Is(err, verror.ErrNoAccess.ID) {
+ if _, _, err := b("bini/shared").Stat(otherCtx); !verror.Is(err, verror.ErrNoAccess.ID) {
t.Fatalf("Stat() should have failed but didn't: %v", err)
}
vlog.VI(2).Infof("Self sets a root ACL.")
- binary = repository.BinaryClient("bini")
newRootACL := make(access.TaggedACLMap)
for _, tag := range access.AllTypicalTags() {
newRootACL.Add("self/$", string(tag))
}
- if err := binary.SetACL(selfCtx, newRootACL, ""); err != nil {
+ if err := b("bini").SetACL(selfCtx, newRootACL, ""); err != nil {
t.Fatalf("SetACL failed: %v", err)
}
vlog.VI(2).Infof("Verify that other can access bini/shared now but not access bini/private.")
- binary = repository.BinaryClient("bini/shared")
- if _, _, err := binary.Stat(otherCtx); err != nil {
+ if _, _, err := b("bini/shared").Stat(otherCtx); err != nil {
t.Fatalf("Stat() shouldn't have failed: %v", err)
}
- binary = repository.BinaryClient("bini/private")
- if _, _, err := binary.Stat(otherCtx); !verror.Is(err, verror.ErrNoAccess.ID) {
+ if _, _, err := b("bini/private").Stat(otherCtx); !verror.Is(err, verror.ErrNoAccess.ID) {
t.Fatalf("Stat() should have failed but didn't: %v", err)
}
vlog.VI(2).Infof("Other still can't create so Self gives Other right to Create.")
- binary = repository.BinaryClient("bini")
- acl, tag, err := binary.GetACL(selfCtx)
+ acl, tag, err := b("bini").GetACL(selfCtx)
if err != nil {
t.Fatalf("GetACL() failed: %v", err)
}
- acl.Add("self", string("Write"))
- err = binary.SetACL(selfCtx, acl, tag)
+
+ // More than one ACL change will result in the same functional result in
+ // this test: that self/other acquires the right to invoke Create at the
+ // root. In particular:
+ //
+ // a. acl.Add("self", "Write ")
+ // b. acl.Add("self/other", "Write")
+ // c. acl.Add("self/other/$", "Write")
+ //
+ // will all give self/other the right to invoke Create but in the case of
+ // (a) it will also extend this right to self's delegates (because of the
+ // absence of the $) including other and in (b) will also extend the
+ // Create right to all of other's delegates. Since (c) is the minimum
+ // case, use that.
+ acl.Add("self/other/$", string("Write"))
+ err = b("bini").SetACL(selfCtx, acl, tag)
if err != nil {
t.Fatalf("SetACL() failed: %v", err)
}
vlog.VI(2).Infof("Other creates bini/otherbinary")
- binary = repository.BinaryClient("bini/otherbinary")
- if err := binary.Create(otherCtx, 1, repository.MediaInfo{Type: "application/octet-stream"}); err != nil {
+ if err := b("bini/otherbinary").Create(otherCtx, 1, repository.MediaInfo{Type: "application/octet-stream"}); err != nil {
t.Fatalf("Create() failed %v", err)
}
fakeDataOther := testData()
- if streamErr, err := invokeUpload(t, otherCtx, binary, fakeDataOther, 0); streamErr != nil || err != nil {
+ if streamErr, err := invokeUpload(t, otherCtx, b("bini/otherbinary"), fakeDataOther, 0); streamErr != nil || err != nil {
t.FailNow()
}
@@ -306,7 +310,7 @@
"Debug": access.ACL{In: []security.BlessingPattern{"self/$", "self/other"}},
"Resolve": access.ACL{In: []security.BlessingPattern{"self/$", "self/other"}},
}
- acl, _, err = binary.GetACL(otherCtx)
+ acl, _, err = b("bini/otherbinary").GetACL(otherCtx)
if err != nil {
t.Fatalf("GetACL failed: %v", err)
}
@@ -314,67 +318,96 @@
t.Errorf("got %#v, expected %#v ", got, want)
}
- vlog.VI(2).Infof("Other tries to exclude self by adding self to Read's notin")
- acl, tag, err = binary.GetACL(otherCtx)
+ vlog.VI(2).Infof("Other tries to exclude self by removing self from the ACL set")
+ acl, tag, err = b("bini/otherbinary").GetACL(otherCtx)
if err != nil {
t.Fatalf("GetACL() failed: %v", err)
}
- acl.Blacklist("self", string("Read"))
- err = binary.SetACL(otherCtx, acl, tag)
+ acl.Clear("self/$")
+ err = b("bini/otherbinary").SetACL(otherCtx, acl, tag)
if err != nil {
t.Fatalf("SetACL() failed: %v", err)
}
- vlog.VI(2).Infof("But self's rights are inherited from root so self can still access despite blacklist.")
- binary = repository.BinaryClient("bini/otherbinary")
- if _, _, err := binary.Stat(selfCtx); err != nil {
+ vlog.VI(2).Infof("Verify that other can make this change.")
+ updated = access.TaggedACLMap{
+ "Admin": access.ACL{In: []security.BlessingPattern{"self/other"}},
+ "Read": access.ACL{In: []security.BlessingPattern{"self/other"}},
+ "Write": access.ACL{In: []security.BlessingPattern{"self/other"}},
+ "Debug": access.ACL{In: []security.BlessingPattern{"self/other"}},
+ "Resolve": access.ACL{In: []security.BlessingPattern{"self/other"}},
+ }
+ acl, _, err = b("bini/otherbinary").GetACL(otherCtx)
+ if err != nil {
+ t.Fatalf("GetACL failed: %v", err)
+ }
+ if got, want := acl.Normalize(), updated.Normalize(); !reflect.DeepEqual(want, got) {
+ t.Errorf("got %#v, expected %#v ", got, want)
+ }
+
+ vlog.VI(2).Infof("But self's rights are inherited from root so self can still access despite this.")
+ if _, _, err := b("bini/otherbinary").Stat(selfCtx); err != nil {
t.Fatalf("Stat() shouldn't have failed: %v", err)
}
vlog.VI(2).Infof("Self petulantly blacklists other back.")
- binary = repository.BinaryClient("bini")
- acl, tag, err = binary.GetACL(selfCtx)
+ acl, tag, err = b("bini").GetACL(selfCtx)
if err != nil {
t.Fatalf("GetACL() failed: %v", err)
}
for _, tag := range access.AllTypicalTags() {
acl.Blacklist("self/other", string(tag))
}
- err = binary.SetACL(selfCtx, acl, tag)
+ err = b("bini").SetACL(selfCtx, acl, tag)
if err != nil {
t.Fatalf("SetACL() failed: %v", err)
}
vlog.VI(2).Infof("And now other can do nothing at affecting the root. Other should be penitent.")
- binary = repository.BinaryClient("bini/nototherbinary")
- if err := binary.Create(otherCtx, 1, repository.MediaInfo{Type: "application/octet-stream"}); !verror.Is(err, verror.ErrNoAccess.ID) {
+ if err := b("bini/nototherbinary").Create(otherCtx, 1, repository.MediaInfo{Type: "application/octet-stream"}); !verror.Is(err, verror.ErrNoAccess.ID) {
t.Fatalf("Create() should have failed %v", err)
}
vlog.VI(2).Infof("But other can still access shared.")
- binary = repository.BinaryClient("bini/shared")
- if _, _, err := binary.Stat(otherCtx); err != nil {
+ if _, _, err := b("bini/shared").Stat(otherCtx); err != nil {
t.Fatalf("Stat() should not have failed but did: %v", err)
}
vlog.VI(2).Infof("Self petulantly blacklists other's binary too.")
- binary = repository.BinaryClient("bini/shared")
- acl, tag, err = binary.GetACL(selfCtx)
+ acl, tag, err = b("bini/shared").GetACL(selfCtx)
if err != nil {
t.Fatalf("GetACL() failed: %v", err)
}
for _, tag := range access.AllTypicalTags() {
acl.Blacklist("self/other", string(tag))
}
- err = binary.SetACL(selfCtx, acl, tag)
+ err = b("bini/shared").SetACL(selfCtx, acl, tag)
+ if err != nil {
+ t.Fatalf("SetACL() failed: %v", err)
+ }
+ vlog.VI(2).Infof("And now other can't access shared either.")
+ if _, _, err := b("bini/shared").Stat(otherCtx); !verror.Is(err, verror.ErrNoAccess.ID) {
+ t.Fatalf("Stat() should have failed but didn't: %v", err)
+ }
+ // TODO(rjkroege): Extend the test with a third principal and verify that
+ // new principals can be given Admin perimission at the root.
+
+ vlog.VI(2).Infof("Self feels guilty for petulance and disempowers itself")
+ // TODO(rjkroege,caprita): This is a one-way transition for self. Perhaps it
+ // should not be. Consider adding a factory-reset facility.
+ acl, tag, err = b("bini").GetACL(selfCtx)
+ if err != nil {
+ t.Fatalf("GetACL() failed: %v", err)
+ }
+ acl.Clear("self/$", "Admin")
+ err = b("bini").SetACL(selfCtx, acl, tag)
if err != nil {
t.Fatalf("SetACL() failed: %v", err)
}
- vlog.VI(2).Infof("And now other can't access shared either.")
- binary = repository.BinaryClient("bini/shared")
- if _, _, err := binary.Stat(otherCtx); !verror.Is(err, verror.ErrNoAccess.ID) {
- t.Fatalf("Stat() should have failed but didn't: %v", err)
+ vlog.VI(2).Info("Self can't access other's binary now")
+ if _, _, err := b("bini/otherbinary").Stat(selfCtx); err == nil {
+ t.Fatalf("Stat() should have failed but didn't")
}
}
@@ -405,8 +438,7 @@
pid := mgmttest.ReadPID(t, nms)
defer syscall.Kill(pid, syscall.SIGINT)
- binary := repository.BinaryClient("bini")
- acl, tag, err := binary.GetACL(selfCtx)
+ acl, tag, err := b("bini").GetACL(selfCtx)
if err != nil {
t.Fatalf("GetACL failed: %#v", err)
}
@@ -422,12 +454,12 @@
}
acl.Blacklist("self", string("Read"))
- err = binary.SetACL(selfCtx, acl, tag)
+ err = b("bini").SetACL(selfCtx, acl, tag)
if err != nil {
t.Fatalf("SetACL() failed: %v", err)
}
- acl, tag, err = binary.GetACL(selfCtx)
+ acl, tag, err = b("bini").GetACL(selfCtx)
if err != nil {
t.Fatalf("GetACL failed: %#v", err)
}
diff --git a/services/mgmt/lib/acls/hierarchical_authorizer.go b/services/mgmt/lib/acls/hierarchical_authorizer.go
index 86204b9..5a0dece 100644
--- a/services/mgmt/lib/acls/hierarchical_authorizer.go
+++ b/services/mgmt/lib/acls/hierarchical_authorizer.go
@@ -1,8 +1,6 @@
package acls
import (
- "fmt"
-
"v.io/v23/security"
"v.io/v23/services/security/access"
"v.io/x/lib/vlog"
@@ -11,8 +9,8 @@
// hierarchicalAuthorizer manages a pair of authorizers for two-level
// inheritance of ACLs.
type hierarchicalAuthorizer struct {
- root security.Authorizer
- child security.Authorizer
+ child security.Authorizer
+ rootACL access.ACL
}
// TAMGetter defines an abstract interface that a customer of
@@ -26,6 +24,15 @@
TAMForPath(path string) (access.TaggedACLMap, bool, error)
}
+func mkRootAuth(rootTam access.TaggedACLMap) (security.Authorizer, error) {
+ rootAuth, err := access.TaggedACLAuthorizer(rootTam, access.TypicalTagType())
+ if err != nil {
+ vlog.Errorf("Successfully obtained an ACL from the filesystem but TaggedACLAuthorizer couldn't use it: %v", err)
+ return nil, err
+ }
+ return rootAuth, nil
+}
+
// NewHierarchicalAuthorizer creates a new hierarchicalAuthorizer
func NewHierarchicalAuthorizer(rootDir, childDir string, get TAMGetter) (security.Authorizer, error) {
rootTam, intentionallyEmpty, err := get.TAMForPath(rootDir)
@@ -36,15 +43,9 @@
return nil, nil
}
- rootAuth, err := access.TaggedACLAuthorizer(rootTam, access.TypicalTagType())
- if err != nil {
- vlog.Errorf("Successfully obtained an ACL from the filesystem but TaggedACLAuthorizer couldn't use it: %v", err)
- return nil, err
- }
-
// We are at the root so exit early.
if rootDir == childDir {
- return rootAuth, nil
+ return mkRootAuth(rootTam)
}
// This is not fatal: the childDir may not exist if we are invoking
@@ -53,7 +54,7 @@
if err != nil {
return nil, err
} else if intentionallyEmpty {
- return rootAuth, nil
+ return mkRootAuth(rootTam)
}
childAuth, err := access.TaggedACLAuthorizer(childTam, access.TypicalTagType())
@@ -63,22 +64,27 @@
}
return &hierarchicalAuthorizer{
- root: rootAuth,
- child: childAuth,
+ child: childAuth,
+ rootACL: rootTam[string(access.Admin)],
}, nil
}
-// Authorize provides two-levels of authorization. Permissions on "Roots"
-// in the namespace will apply to children paths regardless of accesses
-// set on the children. Conversely, ACL exclusions are not inherited.
+// Authorize provides two-levels of authorization. Admin permission
+// on the root provides a "superuser"-like power for administering the
+// server using an instance of hierarchicalAuthorizer. Otherwise, the
+// default permissions of the named path apply.
func (ha *hierarchicalAuthorizer) Authorize(call security.Call) error {
childErr := ha.child.Authorize(call)
if childErr == nil {
return nil
}
- rootErr := ha.root.Authorize(call)
- if rootErr == nil {
+
+ // Maybe the invoking principal can invoke this method because
+ // it has root permissions.
+ names, _ := call.RemoteBlessings().ForCall(call)
+ if len(names) > 0 && ha.rootACL.Includes(names...) {
return nil
}
- return fmt.Errorf("Both root acls (%v) and child acls (%v) deny access.", rootErr, childErr)
+
+ return childErr
}