veyron/services/wsprd: Added an JSIdentityStore that will back the
PublicID handles that are passed to Javascript as well as implemented
support for blessing identities.
Change-Id: I188597e54923cc88bb7d9c509de705fd29eb09eb
diff --git a/services/wsprd/app/app.go b/services/wsprd/app/app.go
index abe25d9..f70647d 100644
--- a/services/wsprd/app/app.go
+++ b/services/wsprd/app/app.go
@@ -8,7 +8,10 @@
"fmt"
"io"
"sync"
+ "time"
+ "veyron/security/caveat"
+ "veyron/services/wsprd/identity"
"veyron/services/wsprd/ipc/client"
"veyron/services/wsprd/ipc/server"
"veyron/services/wsprd/ipc/stream"
@@ -18,6 +21,7 @@
"veyron2/context"
"veyron2/ipc"
"veyron2/rt"
+ "veyron2/security"
"veyron2/verror"
"veyron2/vlog"
"veyron2/vom"
@@ -54,6 +58,26 @@
inType vom.Type
}
+type jsonServiceCaveat struct {
+ Type string `json:"_type"`
+ Service security.PrincipalPattern
+ Data json.RawMessage
+}
+
+type blessingRequest struct {
+ Handle int64
+ Caveats []jsonServiceCaveat
+ DurationMs int64
+ Name string
+}
+
+// PublicIDHandle is a handle given to Javascript that is linked
+// to a PublicID in go.
+type PublicIDHandle struct {
+ Handle int64
+ Names []string
+}
+
// Controller represents all the state of a Veyron Web App. This is the struct
// that is in charge performing all the veyron options.
type Controller struct {
@@ -91,6 +115,9 @@
client ipc.Client
veyronProxyEP string
+
+ // Store for all the PublicIDs that javascript has a handle to.
+ idStore *identity.JSPublicIDHandles
}
// NewController creates a new Controller. writerCreator will be used to create a new flow for rpcs to
@@ -113,6 +140,7 @@
client: client,
writerCreator: writerCreator,
veyronProxyEP: veyronProxyEP,
+ idStore: identity.NewJSPublicIDHandles(),
}
controller.setup()
return controller, nil
@@ -215,6 +243,14 @@
return c.rt
}
+// AddIdentity adds the PublicID to the local id store and returns
+// the handle to it. This function exists because JS only has
+// a handle to the PublicID to avoid shipping the blessing forest
+// to JS and back.
+func (c *Controller) AddIdentity(id security.PublicID) int64 {
+ return c.idStore.Add(id)
+}
+
// Cleanup cleans up any outstanding rpcs.
func (c *Controller) Cleanup() {
c.logger.VI(0).Info("Cleaning up websocket")
@@ -405,7 +441,6 @@
// HandleStopRequest takes a request to stop a server.
func (c *Controller) HandleStopRequest(data string, w lib.ClientWriter) {
-
var serverId uint64
decoder := json.NewDecoder(bytes.NewBufferString(data))
if err := decoder.Decode(&serverId); err != nil {
@@ -529,3 +564,87 @@
return
}
}
+
+// HandleUnlinkJSIdentity removes an identity from the JS identity store.
+// data should be JSON encoded number
+func (c *Controller) HandleUnlinkJSIdentity(data string, w lib.ClientWriter) {
+ decoder := json.NewDecoder(bytes.NewBufferString(data))
+ var handle int64
+ if err := decoder.Decode(&handle); err != nil {
+ w.Error(verror.Internalf("can't unmarshal JSONMessage: %v", err))
+ return
+ }
+ c.idStore.Remove(handle)
+}
+
+// Convert the json wire format of a caveat into the right go object
+func decodeCaveat(c jsonServiceCaveat) (security.ServiceCaveat, error) {
+ var ret security.ServiceCaveat
+ ret.Service = c.Service
+ switch c.Type {
+ case "MethodCaveat":
+ ret.Caveat = &caveat.MethodRestriction{}
+ case "PeerIdentityCaveat":
+ ret.Caveat = &caveat.PeerIdentity{}
+ default:
+ return ret, verror.BadArgf("unknown caveat type %s", c.Type)
+ }
+ return ret, json.Unmarshal(c.Data, &ret.Caveat)
+}
+
+func (c *Controller) getPublicIDHandle(handle int64) (*PublicIDHandle, error) {
+ id := c.idStore.Get(handle)
+ if id == nil {
+ return nil, verror.NotFoundf("uknown public id")
+ }
+ return &PublicIDHandle{Handle: handle, Names: id.Names()}, nil
+}
+
+func (c *Controller) bless(request blessingRequest) (*PublicIDHandle, error) {
+ var caveats []security.ServiceCaveat
+ for _, c := range request.Caveats {
+ newCaveat, err := decodeCaveat(c)
+ if err != nil {
+ return nil, verror.BadArgf("failed to parse caveat: %v", err)
+ }
+ caveats = append(caveats, newCaveat)
+ }
+ duration := time.Duration(request.DurationMs) * time.Millisecond
+
+ blessee := c.idStore.Get(request.Handle)
+
+ if blessee == nil {
+ return nil, verror.NotFoundf("invalid PublicID handle")
+ }
+ blessor := c.rt.Identity()
+
+ blessed, err := blessor.Bless(blessee, request.Name, duration, caveats)
+ if err != nil {
+ return nil, err
+ }
+
+ return &PublicIDHandle{Handle: c.idStore.Add(blessed), Names: blessed.Names()}, nil
+}
+
+// HandleBlessing handles a blessing request from JS.
+func (c *Controller) HandleBlessing(data string, w lib.ClientWriter) {
+ var request blessingRequest
+ decoder := json.NewDecoder(bytes.NewBufferString(data))
+ if err := decoder.Decode(&request); err != nil {
+ w.Error(verror.Internalf("can't unmarshall message: %v", err))
+ return
+ }
+
+ handle, err := c.bless(request)
+
+ if err != nil {
+ w.Error(err)
+ return
+ }
+
+ // Send the id back.
+ if err := w.Send(lib.ResponseFinal, handle); err != nil {
+ w.Error(verror.Internalf("error marshalling results: %v", err))
+ return
+ }
+}
diff --git a/services/wsprd/app/app_test.go b/services/wsprd/app/app_test.go
index 930f79d..0368530 100644
--- a/services/wsprd/app/app_test.go
+++ b/services/wsprd/app/app_test.go
@@ -5,9 +5,11 @@
"encoding/json"
"fmt"
"reflect"
+ "strings"
"sync"
"testing"
"time"
+ "veyron/security/caveat"
"veyron/services/wsprd/ipc/client"
"veyron/services/wsprd/lib"
"veyron/services/wsprd/signature"
@@ -15,6 +17,7 @@
"veyron2/ipc"
"veyron2/naming"
"veyron2/rt"
+ "veyron2/security"
"veyron2/vdl/vdlutil"
"veyron2/verror"
"veyron2/vlog"
@@ -26,6 +29,12 @@
mounttable "veyron/services/mounttable/lib"
)
+var (
+ ctxFooAlice = makeMockSecurityContext("Foo", "test/alice")
+ ctxBarAlice = makeMockSecurityContext("Bar", "test/alice")
+ ctxFooBob = makeMockSecurityContext("Foo", "test/bob")
+ ctxBarBob = makeMockSecurityContext("Bar", "test/bob")
+)
var r = rt.Init()
type simpleAdder struct{}
@@ -559,7 +568,8 @@
"Name": "adder",
"Suffix": "adder",
"RemoteID": map[string]interface{}{
- "Names": names,
+ "Handle": 1.0,
+ "Names": names,
},
},
},
@@ -674,4 +684,258 @@
})
}
-// TODO(bjornick): Make sure that send on stream is nonblocking
+func TestDeserializeCaveat(t *testing.T) {
+ testCases := []struct {
+ json string
+ expectedValue security.ServiceCaveat
+ }{
+ {
+ json: `{"_type":"MethodCaveat","service":"*","data":["Get","MultiGet"]}`,
+ expectedValue: security.ServiceCaveat{
+ Service: "*",
+ Caveat: &caveat.MethodRestriction{"Get", "MultiGet"},
+ },
+ },
+ {
+ json: `{"_type":"PeerIdentityCaveat","service":"*","data":["veyron/batman","veyron/brucewayne"]}`,
+ expectedValue: security.ServiceCaveat{
+ Service: "*",
+ Caveat: &caveat.PeerIdentity{"veyron/batman", "veyron/brucewayne"},
+ },
+ },
+ }
+
+ for _, c := range testCases {
+ var s jsonServiceCaveat
+ if err := json.Unmarshal([]byte(c.json), &s); err != nil {
+ t.Errorf("Failed to deserialize object: %v", err)
+ return
+ }
+
+ caveat, err := decodeCaveat(s)
+ if err != nil {
+ t.Errorf("Failed to convert json caveat to go object: %v")
+ return
+ }
+
+ if !reflect.DeepEqual(caveat, c.expectedValue) {
+ t.Errorf("decoded produced the wrong value: got %v, expected %v", caveat, c.expectedValue)
+ }
+ }
+}
+
+func createChain(r veyron2.Runtime, name string) security.PrivateID {
+ id := r.Identity()
+
+ for _, component := range strings.Split(name, "/") {
+ newID, err := r.NewIdentity(component)
+ if err != nil {
+ panic(err)
+ }
+ if id == nil {
+ id = newID
+ continue
+ }
+ blessedID, err := id.Bless(newID.PublicID(), component, time.Hour, nil)
+ if err != nil {
+ panic(err)
+ }
+ id, err = newID.Derive(blessedID)
+ if err != nil {
+ panic(err)
+ }
+ }
+ return id
+}
+
+type mockSecurityContext struct {
+ method string
+ localID security.PublicID
+}
+
+func makeMockSecurityContext(method string, name string) *mockSecurityContext {
+ return &mockSecurityContext{
+ method: method,
+ localID: createChain(r, name).PublicID(),
+ }
+}
+
+func (m *mockSecurityContext) Method() string { return m.method }
+
+func (m *mockSecurityContext) LocalID() security.PublicID { return m.localID }
+
+func (*mockSecurityContext) Name() string { return "" }
+
+func (*mockSecurityContext) Suffix() string { return "" }
+
+func (*mockSecurityContext) Label() security.Label { return 0 }
+
+func (*mockSecurityContext) CaveatDischarges() security.CaveatDischargeMap { return nil }
+
+func (*mockSecurityContext) RemoteID() security.PublicID { return nil }
+
+func (*mockSecurityContext) LocalEndpoint() naming.Endpoint { return nil }
+
+func (*mockSecurityContext) RemoteEndpoint() naming.Endpoint { return nil }
+
+type blessingTestCase struct {
+ requestJSON map[string]interface{}
+ expectedValidateResults map[*mockSecurityContext]bool
+ expectedErr error
+}
+
+func runBlessingTest(c blessingTestCase, t *testing.T) {
+ controller, err := NewController(nil, "mockVeyronProxyEP")
+
+ if err != nil {
+ t.Errorf("unable to create controller: %v", err)
+ return
+ }
+ controller.AddIdentity(createChain(rt.R(), "test/bar").PublicID())
+
+ bytes, err := json.Marshal(c.requestJSON)
+
+ if err != nil {
+ t.Errorf("failed to marshal request: %v", err)
+ return
+ }
+
+ var request blessingRequest
+ if err := json.Unmarshal(bytes, &request); err != nil {
+ t.Errorf("failed to unmarshal request: %v", err)
+ return
+ }
+
+ jsId, err := controller.bless(request)
+
+ if !reflect.DeepEqual(err, c.expectedErr) {
+ t.Errorf("error response does not match: expected %v, got %v", c.expectedErr, err)
+ return
+ }
+
+ if err != nil {
+ return
+ }
+
+ id := controller.idStore.Get(jsId.Handle)
+
+ if id == nil {
+ t.Errorf("couldn't get identity from store")
+ return
+ }
+
+ for ctx, value := range c.expectedValidateResults {
+ _, err := id.Authorize(ctx)
+ if (err == nil) != value {
+ t.Errorf("authorize failed to match expected value for %v: expected %v, got %v", ctx, value, err)
+ }
+ }
+}
+
+// The names of the identity in the mock contexts are root off the runtime's
+// identity. This function takes a name and prepends the runtime's identity's
+// name.
+func securityName(name string) string {
+ return rt.R().Identity().PublicID().Names()[0] + "/" + name
+}
+
+func TestBlessingWithNoCaveats(t *testing.T) {
+ runBlessingTest(blessingTestCase{
+ requestJSON: map[string]interface{}{
+ "handle": 1,
+ "durationMs": 10000,
+ "name": "foo",
+ },
+ expectedValidateResults: map[*mockSecurityContext]bool{
+ ctxFooAlice: true,
+ ctxFooBob: true,
+ ctxBarAlice: true,
+ ctxBarBob: true,
+ },
+ }, t)
+}
+
+func TestBlessingWithMethodRestrictions(t *testing.T) {
+ runBlessingTest(blessingTestCase{
+ requestJSON: map[string]interface{}{
+ "handle": 1,
+ "durationMs": 10000,
+ "caveats": []map[string]interface{}{
+ map[string]interface{}{
+ "_type": "MethodCaveat",
+ "service": "*",
+ "data": []string{"Foo"},
+ },
+ },
+ "name": "foo",
+ },
+ expectedValidateResults: map[*mockSecurityContext]bool{
+ ctxFooAlice: true,
+ ctxFooBob: true,
+ ctxBarAlice: false,
+ ctxBarBob: false,
+ },
+ }, t)
+}
+
+func TestBlessingWithPeerRestrictions(t *testing.T) {
+ runBlessingTest(blessingTestCase{
+ requestJSON: map[string]interface{}{
+ "handle": 1,
+ "durationMs": 10000,
+ "caveats": []map[string]interface{}{
+ map[string]interface{}{
+ "_type": "PeerIdentityCaveat",
+ "service": "*",
+ "data": []string{securityName("test/alice")},
+ },
+ },
+ "name": "foo",
+ },
+ expectedValidateResults: map[*mockSecurityContext]bool{
+ ctxFooAlice: true,
+ ctxFooBob: false,
+ ctxBarAlice: true,
+ ctxBarBob: false,
+ },
+ }, t)
+}
+
+func TestBlessingWithMethodAndPeerRestrictions(t *testing.T) {
+ runBlessingTest(blessingTestCase{
+ requestJSON: map[string]interface{}{
+ "handle": 1,
+ "durationMs": 10000,
+ "caveats": []map[string]interface{}{
+ map[string]interface{}{
+ "_type": "PeerIdentityCaveat",
+ "service": "*",
+ "data": []string{securityName("test/alice")},
+ },
+ map[string]interface{}{
+ "_type": "MethodCaveat",
+ "service": "*",
+ "data": []string{"Bar"},
+ },
+ },
+ "name": "foo",
+ },
+ expectedValidateResults: map[*mockSecurityContext]bool{
+ ctxFooAlice: false,
+ ctxFooBob: false,
+ ctxBarAlice: true,
+ ctxBarBob: false,
+ },
+ }, t)
+}
+
+func TestBlessingWhereBlesseeDoesNotExist(t *testing.T) {
+ runBlessingTest(blessingTestCase{
+ requestJSON: map[string]interface{}{
+ "handle": 2,
+ "durationMs": 10000,
+ "name": "foo",
+ },
+ expectedErr: verror.NotFoundf("invalid PublicID handle"),
+ }, t)
+}
diff --git a/services/wsprd/identity/js_identity_store.go b/services/wsprd/identity/js_identity_store.go
new file mode 100644
index 0000000..358228b
--- /dev/null
+++ b/services/wsprd/identity/js_identity_store.go
@@ -0,0 +1,46 @@
+package identity
+
+import "veyron2/security"
+import "sync"
+
+// JSPublicIDHandles is a store for PublicIDs in use by JS code.
+// We don't pass the full PublicID to avoid serializing and deserializing a
+// potentially huge forest of blessings. Instead we pass to JS a handle to a public
+// identity and have all operations involve cryptographic operations call into go.
+type JSPublicIDHandles struct {
+ mu sync.Mutex
+ lastHandle int64
+ store map[int64]security.PublicID
+}
+
+// NewJSPublicIDHandles returns a newly initialized JSPublicIDHandles
+func NewJSPublicIDHandles() *JSPublicIDHandles {
+ return &JSPublicIDHandles{
+ store: map[int64]security.PublicID{},
+ }
+}
+
+// Add adds a PublicID to the store and returns the handle to it.
+func (s *JSPublicIDHandles) Add(identity security.PublicID) int64 {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.lastHandle++
+ handle := s.lastHandle
+ s.store[handle] = identity
+ return handle
+}
+
+// Remove removes the PublicID associated with the handle.
+func (s *JSPublicIDHandles) Remove(handle int64) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ delete(s.store, handle)
+}
+
+// Get returns the PublicID represented by the handle. Returns nil
+// if no PublicID exists for the handle.
+func (s *JSPublicIDHandles) Get(handle int64) security.PublicID {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ return s.store[handle]
+}
diff --git a/services/wsprd/ipc/server/server.go b/services/wsprd/ipc/server/server.go
index 989a053..737d085 100644
--- a/services/wsprd/ipc/server/server.go
+++ b/services/wsprd/ipc/server/server.go
@@ -34,7 +34,8 @@
}
type publicID struct {
- Names []string
+ Handle int64
+ Names []string
}
// call context for a serverRPCRequest
@@ -50,10 +51,20 @@
Err *verror.Standard
}
-type ServerHelper interface {
+type FlowHandler interface {
CreateNewFlow(server *Server, sender stream.Sender) *Flow
CleanupFlow(id int64)
+}
+
+type HandleStore interface {
+ // Adds an identity to the store and returns handle to the identity
+ AddIdentity(identity security.PublicID) int64
+}
+
+type ServerHelper interface {
+ FlowHandler
+ HandleStore
GetLogger() vlog.Logger
@@ -110,11 +121,13 @@
s.Lock()
s.outstandingServerRequests[flow.ID] = replyChan
s.Unlock()
+ remoteID := call.RemoteID()
context := serverRPCRequestContext{
Suffix: call.Suffix(),
Name: call.Name(),
RemoteID: publicID{
- Names: call.RemoteID().Names(),
+ Handle: s.helper.AddIdentity(remoteID),
+ Names: remoteID.Names(),
},
}
// Send a invocation request to JavaScript
diff --git a/services/wsprd/wspr/pipe.go b/services/wsprd/wspr/pipe.go
index 14be0eb..73e5f08 100644
--- a/services/wsprd/wspr/pipe.go
+++ b/services/wsprd/wspr/pipe.go
@@ -49,6 +49,13 @@
// A request to associate an identity with an origin
websocketAssocIdentity = 7
+
+ // A request to bless an identity
+ websocketBlessIdentity = 8
+
+ // A request to unlink an identity. This request means that
+ // we can remove the given handle from the handle store.
+ websocketUnlinkIdentity = 9
)
type websocketMessage struct {
@@ -260,6 +267,10 @@
// from javascript.
ctx := p.wspr.rt.NewContext()
go p.controller.HandleSignatureRequest(ctx, msg.Data, ww)
+ case websocketBlessIdentity:
+ go p.controller.HandleBlessing(msg.Data, ww)
+ case websocketUnlinkIdentity:
+ go p.controller.HandleUnlinkJSIdentity(msg.Data, ww)
default:
ww.Error(verror.Unknownf("unknown message type: %v", msg.Type))
}