Merge "veyron.io/veyron/veyron2/i18n: Initialize default i18n catalogue from file"
diff --git a/runtimes/google/ipc/blessings_cache.go b/runtimes/google/ipc/blessings_cache.go
new file mode 100644
index 0000000..ff6a93b
--- /dev/null
+++ b/runtimes/google/ipc/blessings_cache.go
@@ -0,0 +1,153 @@
+package ipc
+
+import (
+ "fmt"
+ "reflect"
+ "sync"
+
+ "veyron.io/veyron/veyron2/ipc"
+ "veyron.io/veyron/veyron2/ipc/stream"
+ "veyron.io/veyron/veyron2/security"
+)
+
+// clientEncodeBlessings gets or inserts the blessings into the cache.
+func clientEncodeBlessings(cache stream.VCDataCache, blessings security.Blessings) ipc.BlessingsRequest {
+ blessingsCacheAny := cache.GetOrInsert(clientBlessingsKey{}, newClientBlessingsCache)
+ blessingsCache := blessingsCacheAny.(*clientBlessingsCache)
+ return blessingsCache.getOrInsert(blessings)
+}
+
+// clientAckBlessings verifies that the server has updated its cache to include blessings.
+// This means that subsequent rpcs from the client with blessings can send only a cache key.
+func clientAckBlessings(cache stream.VCDataCache, blessings security.Blessings) {
+ blessingsCacheAny := cache.GetOrInsert(clientBlessingsKey{}, newClientBlessingsCache)
+ blessingsCache := blessingsCacheAny.(*clientBlessingsCache)
+ blessingsCache.acknowledge(blessings)
+}
+
+// serverDecodeBlessings insert the key and blessings into the cache or get the blessings if only
+// key is provided in req.
+func serverDecodeBlessings(cache stream.VCDataCache, req ipc.BlessingsRequest, stats *ipcStats) (security.Blessings, error) {
+ blessingsCacheAny := cache.GetOrInsert(serverBlessingsKey{}, newServerBlessingsCache)
+ blessingsCache := blessingsCacheAny.(*serverBlessingsCache)
+ return blessingsCache.getOrInsert(req, stats)
+}
+
+// IMPLEMENTATION DETAILS BELOW
+
+// clientBlessingsCache is a thread-safe map from blessings to cache key.
+type clientBlessingsCache struct {
+ sync.RWMutex
+ m map[security.Blessings]clientCacheValue
+ key uint64
+}
+
+type clientCacheValue struct {
+ key uint64
+ // ack is set to true once the server has confirmed receipt of the cache key.
+ // Clients that insert into the cache when ack is false must send both the key
+ // and the blessings.
+ ack bool
+}
+
+// clientBlessingsKey is the key used to retrieve the clientBlessingsCache from the VCDataCache.
+type clientBlessingsKey struct{}
+
+func newClientBlessingsCache() interface{} {
+ return &clientBlessingsCache{m: make(map[security.Blessings]clientCacheValue)}
+}
+
+func (c *clientBlessingsCache) getOrInsert(blessings security.Blessings) ipc.BlessingsRequest {
+ c.RLock()
+ val, exists := c.m[blessings]
+ c.RUnlock()
+ if exists {
+ return c.makeBlessingsRequest(val, blessings)
+ }
+ // if the val doesn't exist we must create a new key, update the cache, and send the key and blessings.
+ c.Lock()
+ defer c.Unlock()
+ // we must check that the val wasn't inserted in the time we changed locks.
+ val, exists = c.m[blessings]
+ if exists {
+ return c.makeBlessingsRequest(val, blessings)
+ }
+ newVal := clientCacheValue{key: c.nextKeyLocked()}
+ c.m[blessings] = newVal
+ return c.makeBlessingsRequest(newVal, blessings)
+}
+
+func (c *clientBlessingsCache) acknowledge(blessings security.Blessings) {
+ c.Lock()
+ val := c.m[blessings]
+ val.ack = true
+ c.m[blessings] = val
+ c.Unlock()
+}
+
+func (c *clientBlessingsCache) makeBlessingsRequest(val clientCacheValue, blessings security.Blessings) ipc.BlessingsRequest {
+ if val.ack {
+ // when the value is acknowledged, only send the key, since the server has confirmed that it knows the key.
+ return ipc.BlessingsRequest{Key: val.key}
+ }
+ // otherwise we still need to send both key and blessings, but we must ensure that we send the same key.
+ wireBlessings := security.MarshalBlessings(blessings)
+ return ipc.BlessingsRequest{val.key, &wireBlessings}
+}
+
+// nextKeyLocked creates a new key for inserting blessings. It must be called after acquiring a writer lock.
+func (c *clientBlessingsCache) nextKeyLocked() uint64 {
+ c.key++
+ return c.key
+}
+
+// serverBlessingsCache is a thread-safe map from cache key to blessings.
+type serverBlessingsCache struct {
+ sync.RWMutex
+ m map[uint64]security.Blessings
+}
+
+// serverBlessingsKey is the key used to retrieve the serverBlessingsCache from the VCDataCache.
+type serverBlessingsKey struct{}
+
+func newServerBlessingsCache() interface{} {
+ return &serverBlessingsCache{m: make(map[uint64]security.Blessings)}
+}
+
+func (c *serverBlessingsCache) getOrInsert(req ipc.BlessingsRequest, stats *ipcStats) (security.Blessings, error) {
+ // In the case that the key sent is 0, we are running in VCSecurityNone and should
+ // return nil for the client Blessings.
+ if req.Key == 0 {
+ return nil, nil
+ }
+ if req.Blessings == nil {
+ // Fastpath, lookup based on the key.
+ c.RLock()
+ cached, exists := c.m[req.Key]
+ c.RUnlock()
+ if !exists {
+ return nil, fmt.Errorf("ipc: key was not in the cache")
+ }
+ stats.recordBlessingCache(true)
+ return cached, nil
+ }
+ // Slowpath, might need to update the cache, or check that the received blessings are
+ // the same as what's in the cache.
+ recv, err := security.NewBlessings(*req.Blessings)
+ if err != nil {
+ return nil, fmt.Errorf("ipc: create new client blessings failed: %v", err)
+ }
+ c.Lock()
+ defer c.Unlock()
+ if cached, exists := c.m[req.Key]; exists {
+ // TODO(suharshs): Replace this reflect.DeepEqual() with a less expensive check.
+ if !reflect.DeepEqual(cached, recv) {
+ return nil, fmt.Errorf("client sent invalid Blessings")
+ }
+ stats.recordBlessingCache(true)
+ return cached, nil
+ }
+ c.m[req.Key] = recv
+ stats.recordBlessingCache(false)
+ return recv, nil
+}
diff --git a/runtimes/google/ipc/client.go b/runtimes/google/ipc/client.go
index 8475dea..5edc884 100644
--- a/runtimes/google/ipc/client.go
+++ b/runtimes/google/ipc/client.go
@@ -61,7 +61,9 @@
errRequestEncoding = verror.Register(pkgPath+".requestEncoding", verror.NoRetry, "failed to encode request {3}{:4}")
- errDischargeEncoding = verror.Register(pkgPath+".dischargeEncoding", verror.NoRetry, "failed to encode discharge {3}{:4}")
+ errDischargeEncoding = verror.Register(pkgPath+".dischargeEncoding", verror.NoRetry, "failed to encode discharges {:3}")
+
+ errBlessingEncoding = verror.Register(pkgPath+".blessingEncoding", verror.NoRetry, "failed to encode blessing {3}{:4}")
errArgEncoding = verror.Register(pkgPath+".argEncoding", verror.NoRetry, "failed to encode arg #{3}{:4:}")
@@ -656,6 +658,8 @@
discharges []security.Discharge // discharges used for this request
dc vc.DischargeClient // client-global discharge-client
+ blessings security.Blessings // the local blessings for the current RPC.
+
sendClosedMu sync.Mutex
sendClosed bool // is the send side already closed? GUARDED_BY(sendClosedMu)
finished bool // has Finish() already been called?
@@ -703,25 +707,32 @@
if self := fc.flow.LocalBlessings(); self != nil && fc.dc != nil {
fc.discharges = fc.dc.PrepareDischarges(fc.ctx, self.ThirdPartyCaveats(), mkDischargeImpetus(fc.server, method, args))
}
+ // Encode the Blessings information for the client to authorize the flow.
+ var blessingsRequest ipc.BlessingsRequest
+ if fc.flow.LocalPrincipal() != nil {
+ localBlessings := fc.flow.LocalPrincipal().BlessingStore().ForPeer(fc.server...)
+ blessingsRequest = clientEncodeBlessings(fc.flow.VCDataCache(), localBlessings)
+ }
+ // TODO(suharshs, ataly): Make security.Discharge a vdl type.
+ anyDischarges := make([]vdlutil.Any, len(fc.discharges))
+ for i, d := range fc.discharges {
+ anyDischarges[i] = d
+ }
req := ipc.Request{
Suffix: suffix,
Method: method,
NumPosArgs: uint64(len(args)),
Timeout: int64(timeout),
GrantedBlessings: security.MarshalBlessings(blessings),
- NumDischarges: uint64(len(fc.discharges)),
+ Blessings: blessingsRequest,
+ Discharges: anyDischarges,
TraceRequest: ivtrace.Request(fc.ctx),
}
if err := fc.enc.Encode(req); err != nil {
berr := verror.Make(verror.BadProtocol, fc.ctx, verror.Make(errRequestEncoding, fc.ctx, fmt.Sprintf("%#v", req), err))
return fc.close(berr)
}
- for _, d := range fc.discharges {
- if err := fc.enc.Encode(d); err != nil {
- berr := verror.Make(verror.BadProtocol, fc.ctx, verror.Make(errDischargeEncoding, fc.ctx, d.ID(), err))
- return fc.close(berr)
- }
- }
+
for ix, arg := range args {
if err := fc.enc.Encode(arg); err != nil {
berr := verror.Make(verror.BadProtocol, fc.ctx, verror.Make(errArgEncoding, fc.ctx, ix, err))
@@ -874,6 +885,9 @@
return fc.close(berr)
}
}
+ if fc.response.AckBlessings {
+ clientAckBlessings(fc.flow.VCDataCache(), fc.blessings)
+ }
// Incorporate any VTrace info that was returned.
ivtrace.MergeResponse(fc.ctx, &fc.response.TraceResponse)
if fc.response.Error != nil {
diff --git a/runtimes/google/ipc/full_test.go b/runtimes/google/ipc/full_test.go
index bd95b94..6705ef0 100644
--- a/runtimes/google/ipc/full_test.go
+++ b/runtimes/google/ipc/full_test.go
@@ -28,6 +28,7 @@
"veyron.io/veyron/veyron2/vlog"
"veyron.io/veyron/veyron/lib/netstate"
+ "veyron.io/veyron/veyron/lib/stats"
_ "veyron.io/veyron/veyron/lib/tcp"
"veyron.io/veyron/veyron/lib/testutil"
tsecurity "veyron.io/veyron/veyron/lib/testutil/security"
@@ -1589,6 +1590,107 @@
}
}
+// TestBlessingsCache tests that the VCCache is used to sucessfully used to cache duplicate
+// calls blessings.
+func TestBlessingsCache(t *testing.T) {
+ var (
+ pserver = tsecurity.NewPrincipal("server")
+ pclient = tsecurity.NewPrincipal("client")
+ )
+ // Make the client recognize all server blessings
+ if err := pclient.AddToRoots(pserver.BlessingStore().Default()); err != nil {
+ t.Fatal(err)
+ }
+
+ ns := tnaming.NewSimpleNamespace()
+ rid, err := naming.NewRoutingID()
+ if err != nil {
+ t.Fatal(err)
+ }
+ runServer := func(principal security.Principal, rid naming.RoutingID) (ipc.Server, stream.Manager, naming.Endpoint) {
+ sm := imanager.InternalNew(rid)
+ server, err := InternalNewServer(testContext(), sm, ns, nil, vc.LocalPrincipal{principal})
+ if err != nil {
+ t.Fatal(err)
+ }
+ ep, err := server.Listen(listenSpec)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return server, sm, ep[0]
+ }
+
+ server, serverSM, serverEP := runServer(pserver, rid)
+ go server.Serve("mountpoint/testServer", &testServer{}, acceptAllAuthorizer{})
+ defer serverSM.Shutdown()
+
+ newClient := func() ipc.Client {
+ rid, err := naming.NewRoutingID()
+ if err != nil {
+ t.Fatal(err)
+ }
+ smc := imanager.InternalNew(rid)
+ defer smc.Shutdown()
+ client, err := InternalNewClient(smc, ns, vc.LocalPrincipal{pclient})
+ if err != nil {
+ t.Fatalf("failed to create client: %v", err)
+ }
+ return client
+ }
+
+ runClient := func(client ipc.Client) {
+ var call ipc.Call
+ if call, err = client.StartCall(testContext(), "/"+serverEP.String(), "Closure", nil); err != nil {
+ t.Fatalf("failed to StartCall: %v", err)
+ }
+ if err := call.Finish(); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ cachePrefix := naming.Join("ipc", "server", "routing-id", rid.String(), "security", "blessings", "cache")
+ cacheHits, err := stats.GetStatsObject(naming.Join(cachePrefix, "hits"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ cacheAttempts, err := stats.GetStatsObject(naming.Join(cachePrefix, "attempts"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Check that the blessings cache is not used on the first call.
+ clientA := newClient()
+ runClient(clientA)
+ if gotAttempts, gotHits := cacheAttempts.Value().(int64), cacheHits.Value().(int64); gotAttempts != 1 || gotHits != 0 {
+ t.Errorf("got cacheAttempts(%v), cacheHits(%v), expected cacheAttempts(1), cacheHits(0)", gotAttempts, gotHits)
+ }
+ // Check that the cache is hit on the second call with the same blessings.
+ runClient(clientA)
+ if gotAttempts, gotHits := cacheAttempts.Value().(int64), cacheHits.Value().(int64); gotAttempts != 2 || gotHits != 1 {
+ t.Errorf("got cacheAttempts(%v), cacheHits(%v), expected cacheAttempts(2), cacheHits(1)", gotAttempts, gotHits)
+ }
+ clientA.Close()
+ // Check that the cache is not used with a different client.
+ clientB := newClient()
+ runClient(clientB)
+ if gotAttempts, gotHits := cacheAttempts.Value().(int64), cacheHits.Value().(int64); gotAttempts != 3 || gotHits != 1 {
+ t.Errorf("got cacheAttempts(%v), cacheHits(%v), expected cacheAttempts(3), cacheHits(1)", gotAttempts, gotHits)
+ }
+ // clientB changes its blessings, the cache should not be used.
+ blessings, err := pserver.Bless(pclient.PublicKey(), pserver.BlessingStore().Default(), "cav", mkCaveat(security.ExpiryCaveat(time.Now().Add(time.Hour))))
+ if err != nil {
+ t.Fatalf("failed to create Blessings: %v", err)
+ }
+ if _, err = pclient.BlessingStore().Set(blessings, "server"); err != nil {
+ t.Fatalf("failed to set blessings: %v", err)
+ }
+ runClient(clientB)
+ if gotAttempts, gotHits := cacheAttempts.Value().(int64), cacheHits.Value().(int64); gotAttempts != 4 || gotHits != 1 {
+ t.Errorf("got cacheAttempts(%v), cacheHits(%v), expected cacheAttempts(4), cacheHits(1)", gotAttempts, gotHits)
+ }
+ clientB.Close()
+}
+
func init() {
testutil.Init()
vdlutil.Register(fakeTimeCaveat(0))
diff --git a/runtimes/google/ipc/server.go b/runtimes/google/ipc/server.go
index 6fbdd4e..fbc32e7 100644
--- a/runtimes/google/ipc/server.go
+++ b/runtimes/google/ipc/server.go
@@ -17,6 +17,7 @@
"veyron.io/veyron/veyron2/options"
"veyron.io/veyron/veyron2/security"
"veyron.io/veyron/veyron2/services/security/access"
+ "veyron.io/veyron/veyron2/vdl"
old_verror "veyron.io/veyron/veyron2/verror"
verror "veyron.io/veyron/veyron2/verror2"
"veyron.io/veyron/veyron2/vlog"
@@ -794,13 +795,15 @@
flow stream.Flow // underlying flow
// Fields filled in during the server invocation.
- blessings security.Blessings
- method, suffix string
- tags []interface{}
- discharges map[string]security.Discharge
- starttime time.Time
- endStreamArgs bool // are the stream args at EOF?
- allowDebug bool // true if the caller is permitted to view debug information.
+ clientBlessings security.Blessings
+ ackBlessings bool
+ blessings security.Blessings
+ method, suffix string
+ tags []interface{}
+ discharges map[string]security.Discharge
+ starttime time.Time
+ endStreamArgs bool // are the stream args at EOF?
+ allowDebug bool // true if the caller is permitted to view debug information.
}
var _ ipc.Stream = (*flowServer)(nil)
@@ -895,6 +898,7 @@
EndStreamResults: true,
NumPosResults: uint64(len(results)),
TraceResponse: traceResponse,
+ AckBlessings: fs.ackBlessings,
}
if err := fs.enc.Encode(response); err != nil {
if err == io.EOF {
@@ -1080,13 +1084,27 @@
if blessings != nil && !reflect.DeepEqual(blessings.PublicKey(), fs.flow.LocalPrincipal().PublicKey()) {
return old_verror.NoAccessf("ipc: blessing granted not bound to this server(%v vs %v)", blessings.PublicKey(), fs.flow.LocalPrincipal().PublicKey())
}
- // Receive third party caveat discharges the client sent
- for i := uint64(0); i < req.NumDischarges; i++ {
- var d security.Discharge
- if err := fs.dec.Decode(&d); err != nil {
- return old_verror.BadProtocolf("ipc: decoding discharge %d of %d failed: %v", i, req.NumDischarges, err)
+ fs.clientBlessings, err = serverDecodeBlessings(fs.flow.VCDataCache(), req.Blessings, fs.server.stats)
+ if err != nil {
+ // When the server can't access the blessings cache, the client is not following
+ // protocol, so the server closes the VCs corresponding to the client endpoint.
+ // TODO(suharshs,toddw): Figure out a way to only shutdown the current VC, instead
+ // of all VCs connected to the RemoteEndpoint.
+ fs.server.streamMgr.ShutdownEndpoint(fs.RemoteEndpoint())
+ return old_verror.BadProtocolf("ipc: blessings cache failed: %v", err)
+ }
+ fs.ackBlessings = true
+
+ // TODO(suharshs, ataly): Make security.Discharge a vdl type.
+ for i, d := range req.Discharges {
+ if dis, ok := d.(security.Discharge); ok {
+ fs.discharges[dis.ID()] = dis
+ continue
}
- fs.discharges[d.ID()] = d
+ if v, ok := d.(*vdl.Value); ok {
+ return old_verror.BadProtocolf("ipc: discharge #%d of type %s isn't registered", i, v.Type())
+ }
+ return old_verror.BadProtocolf("ipc: discharge #%d of type %T doesn't implement security.Discharge", i, d)
}
return nil
}
@@ -1193,6 +1211,9 @@
}
func (fs *flowServer) RemoteBlessings() security.Blessings {
//nologcall
+ if fs.clientBlessings != nil {
+ return fs.clientBlessings
+ }
return fs.flow.RemoteBlessings()
}
func (fs *flowServer) Blessings() security.Blessings {
diff --git a/runtimes/google/ipc/stats.go b/runtimes/google/ipc/stats.go
index 334ce23..ea5441f 100644
--- a/runtimes/google/ipc/stats.go
+++ b/runtimes/google/ipc/stats.go
@@ -5,19 +5,25 @@
"time"
"veyron.io/veyron/veyron/lib/stats"
+ "veyron.io/veyron/veyron/lib/stats/counter"
"veyron.io/veyron/veyron/lib/stats/histogram"
"veyron.io/veyron/veyron2/naming"
)
type ipcStats struct {
- mu sync.RWMutex
- prefix string
- methods map[string]*perMethodStats
+ mu sync.RWMutex
+ prefix string
+ methods map[string]*perMethodStats
+ blessingsCacheStats *blessingsCacheStats
}
func newIPCStats(prefix string) *ipcStats {
- return &ipcStats{prefix: prefix, methods: make(map[string]*perMethodStats)}
+ return &ipcStats{
+ prefix: prefix,
+ methods: make(map[string]*perMethodStats),
+ blessingsCacheStats: newBlessingsCacheStats(prefix),
+ }
}
type perMethodStats struct {
@@ -41,6 +47,10 @@
m.latency.Add(int64(latency / time.Millisecond))
}
+func (s *ipcStats) recordBlessingCache(hit bool) {
+ s.blessingsCacheStats.incr(hit)
+}
+
// newPerMethodStats creates a new perMethodStats object if one doesn't exist
// already. It returns the newly created object, or the already existing one.
func (s *ipcStats) newPerMethodStats(method string) *perMethodStats {
@@ -61,3 +71,25 @@
}
return m
}
+
+// blessingsCacheStats keeps blessing cache hits and total calls received to determine
+// how often the blessingCache is being used.
+type blessingsCacheStats struct {
+ callsReceived, cacheHits *counter.Counter
+}
+
+func newBlessingsCacheStats(prefix string) *blessingsCacheStats {
+ cachePrefix := naming.Join(prefix, "security", "blessings", "cache")
+ return &blessingsCacheStats{
+ callsReceived: stats.NewCounter(naming.Join(cachePrefix, "attempts")),
+ cacheHits: stats.NewCounter(naming.Join(cachePrefix, "hits")),
+ }
+}
+
+// Incr increments the cache attempt counter and the cache hit counter if hit is true.
+func (s *blessingsCacheStats) incr(hit bool) {
+ s.callsReceived.Incr(1)
+ if hit {
+ s.cacheHits.Incr(1)
+ }
+}
diff --git a/runtimes/google/ipc/stream/manager/listener.go b/runtimes/google/ipc/stream/manager/listener.go
index bd6d272..ace828f 100644
--- a/runtimes/google/ipc/stream/manager/listener.go
+++ b/runtimes/google/ipc/stream/manager/listener.go
@@ -17,7 +17,7 @@
"veyron.io/veyron/veyron2/naming"
"veyron.io/veyron/veyron2/verror"
"veyron.io/veyron/veyron2/vlog"
- "veyron.io/veyron/veyron2/vom"
+ "veyron.io/veyron/veyron2/vom2"
)
var errListenerIsClosed = errors.New("Listener has been Closed")
@@ -188,12 +188,24 @@
}
var request proxy.Request
var response proxy.Response
- if err := vom.NewEncoder(flow).Encode(request); err != nil {
+ enc, err := vom2.NewBinaryEncoder(flow)
+ if err != nil {
+ flow.Close()
+ vf.StopAccepting()
+ return nil, nil, fmt.Errorf("failed to create new Encoder: %v", err)
+ }
+ if err := enc.Encode(request); err != nil {
flow.Close()
vf.StopAccepting()
return nil, nil, fmt.Errorf("failed to encode request to proxy: %v", err)
}
- if err := vom.NewDecoder(flow).Decode(&response); err != nil {
+ dec, err := vom2.NewDecoder(flow)
+ if err != nil {
+ flow.Close()
+ vf.StopAccepting()
+ return nil, nil, fmt.Errorf("failed to create new Decoder: %v", err)
+ }
+ if err := dec.Decode(&response); err != nil {
flow.Close()
vf.StopAccepting()
return nil, nil, fmt.Errorf("failed to decode response from proxy: %v", err)
diff --git a/runtimes/google/ipc/stream/proxy/proxy.go b/runtimes/google/ipc/stream/proxy/proxy.go
index 095357b..46cc968 100644
--- a/runtimes/google/ipc/stream/proxy/proxy.go
+++ b/runtimes/google/ipc/stream/proxy/proxy.go
@@ -11,7 +11,7 @@
"veyron.io/veyron/veyron2/security"
"veyron.io/veyron/veyron2/verror"
"veyron.io/veyron/veyron2/vlog"
- "veyron.io/veyron/veyron2/vom"
+ "veyron.io/veyron/veyron2/vom2"
"veyron.io/veyron/veyron/runtimes/google/ipc/stream/crypto"
"veyron.io/veyron/veyron/runtimes/google/ipc/stream/id"
@@ -398,7 +398,10 @@
server.Process.InitVCI(server.VC.VCI())
var request Request
var response Response
- if err := vom.NewDecoder(conn).Decode(&request); err != nil {
+ dec, err := vom2.NewDecoder(conn)
+ if err != nil {
+ response.Error = verror.BadProtocolf("proxy: failed to create Decoder: %v", err)
+ } else if err := dec.Decode(&request); err != nil {
response.Error = verror.BadProtocolf("proxy: unable to read Request: %v", err)
} else if err := p.servers.Add(server); err != nil {
response.Error = verror.Convert(err)
@@ -412,7 +415,13 @@
response.Endpoint = ep.String()
}
}
- if err := vom.NewEncoder(conn).Encode(response); err != nil {
+ enc, err := vom2.NewBinaryEncoder(conn)
+ if err != nil {
+ proxyLog().Infof("Failed to create Encoder for server %v: %v", server, err)
+ server.Close(err)
+ return
+ }
+ if err := enc.Encode(response); err != nil {
proxyLog().Infof("Failed to encode response %#v for server %v", response, server)
server.Close(err)
return
diff --git a/runtimes/google/ipc/stream/vc/auth.go b/runtimes/google/ipc/stream/vc/auth.go
index 5ecc6ca..7513f04 100644
--- a/runtimes/google/ipc/stream/vc/auth.go
+++ b/runtimes/google/ipc/stream/vc/auth.go
@@ -12,7 +12,7 @@
"veyron.io/veyron/veyron2/context"
"veyron.io/veyron/veyron2/ipc/version"
"veyron.io/veyron/veyron2/security"
- "veyron.io/veyron/veyron2/vom"
+ "veyron.io/veyron/veyron2/vom2"
)
var (
@@ -94,7 +94,10 @@
return err
}
var buf bytes.Buffer
- enc := vom.NewEncoder(&buf)
+ enc, err := vom2.NewBinaryEncoder(&buf)
+ if err != nil {
+ return err
+ }
if err := enc.Encode(signature); err != nil {
return err
}
@@ -111,12 +114,20 @@
return err
}
defer msg.Release()
- return vom.NewEncoder(w).Encode(msg.Contents)
+ enc, err = vom2.NewBinaryEncoder(w)
+ if err != nil {
+ return err
+ }
+ return enc.Encode(msg.Contents)
}
func readBlessings(r io.Reader, tag []byte, crypter crypto.Crypter, v version.IPCVersion) (blessings security.Blessings, discharges map[string]security.Discharge, err error) {
var msg []byte
- if err = vom.NewDecoder(r).Decode(&msg); err != nil {
+ dec, err := vom2.NewDecoder(r)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to create new decoder: %v", err)
+ }
+ if err = dec.Decode(&msg); err != nil {
return nil, nil, fmt.Errorf("failed to read handshake message: %v", err)
}
buf, err := crypter.Decrypt(iobuf.NewSlice(msg))
@@ -124,7 +135,10 @@
return
}
defer buf.Release()
- dec := vom.NewDecoder(bytes.NewReader(buf.Contents))
+ dec, err = vom2.NewDecoder(bytes.NewReader(buf.Contents))
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to create new decoder: %v", err)
+ }
var (
wireb security.WireBlessings
diff --git a/runtimes/google/ipc/stream/vc/data_cache.go b/runtimes/google/ipc/stream/vc/data_cache.go
new file mode 100644
index 0000000..8b75ed0
--- /dev/null
+++ b/runtimes/google/ipc/stream/vc/data_cache.go
@@ -0,0 +1,40 @@
+package vc
+
+import (
+ "sync"
+)
+
+// dataCache is a thread-safe map for any two types.
+type dataCache struct {
+ sync.RWMutex
+ m map[interface{}]interface{}
+}
+
+func newDataCache() *dataCache {
+ return &dataCache{m: make(map[interface{}]interface{})}
+}
+
+// GetOrInsert first checks if the key exists in the cache with a reader lock.
+// If it doesn't exist, it instead acquires a writer lock, creates and stores the new value
+// with create and returns value.
+func (c *dataCache) GetOrInsert(key interface{}, create func() interface{}) interface{} {
+ // We use the read lock for the fastpath. This should be the more common case, so we rarely
+ // need a writer lock.
+ c.RLock()
+ value, exists := c.m[key]
+ c.RUnlock()
+ if exists {
+ return value
+ }
+ // We acquire the writer lock for the slowpath, and need to re-check if the key exists
+ // in the map, since other thread may have snuck in.
+ c.Lock()
+ defer c.Unlock()
+ value, exists = c.m[key]
+ if exists {
+ return value
+ }
+ value = create()
+ c.m[key] = value
+ return value
+}
diff --git a/runtimes/google/ipc/stream/vc/flow.go b/runtimes/google/ipc/stream/vc/flow.go
index 958db51..8e32596 100644
--- a/runtimes/google/ipc/stream/vc/flow.go
+++ b/runtimes/google/ipc/stream/vc/flow.go
@@ -1,6 +1,7 @@
package vc
import (
+ "veyron.io/veyron/veyron2/ipc/stream"
"veyron.io/veyron/veyron2/naming"
"veyron.io/veyron/veyron2/security"
)
@@ -10,6 +11,7 @@
*reader
*writer
localEndpoint, remoteEndpoint naming.Endpoint
+ dataCache *dataCache
}
type authN interface {
@@ -48,3 +50,9 @@
f.reader.Close()
f.writer.shutdown(false)
}
+
+// VCDataCache returns the stream.VCDataCache object that allows information to be
+// shared across the Flow's parent VC.
+func (f *flow) VCDataCache() stream.VCDataCache {
+ return f.dataCache
+}
diff --git a/runtimes/google/ipc/stream/vc/listener_test.go b/runtimes/google/ipc/stream/vc/listener_test.go
index 098ff33..3f42607 100644
--- a/runtimes/google/ipc/stream/vc/listener_test.go
+++ b/runtimes/google/ipc/stream/vc/listener_test.go
@@ -3,6 +3,7 @@
import (
"testing"
+ "veyron.io/veyron/veyron2/ipc/stream"
"veyron.io/veyron/veyron2/naming"
"veyron.io/veyron/veyron2/security"
)
@@ -25,6 +26,7 @@
func (*noopFlow) RemoteBlessings() security.Blessings { return nil }
func (*noopFlow) RemoteDischarges() map[string]security.Discharge { return nil }
func (*noopFlow) SetDeadline(<-chan struct{}) {}
+func (*noopFlow) VCDataCache() stream.VCDataCache { return nil }
func TestListener(t *testing.T) {
ln := newListener()
diff --git a/runtimes/google/ipc/stream/vc/vc.go b/runtimes/google/ipc/stream/vc/vc.go
index 38d8db6..b522f9e 100644
--- a/runtimes/google/ipc/stream/vc/vc.go
+++ b/runtimes/google/ipc/stream/vc/vc.go
@@ -61,8 +61,9 @@
crypter crypto.Crypter
closeReason string // reason why the VC was closed
- helper Helper
- version version.IPCVersion
+ helper Helper
+ version version.IPCVersion
+ dataCache *dataCache // dataCache contains information that can shared between Flows from this VC.
}
// NoDischarges specifies that the RPC call should not fetch discharges.
@@ -164,6 +165,7 @@
crypter: crypto.NewNullCrypter(),
helper: p.Helper,
version: p.Version,
+ dataCache: newDataCache(),
}
}
@@ -183,6 +185,7 @@
writer: writer,
localEndpoint: vc.localEP,
remoteEndpoint: vc.remoteEP,
+ dataCache: vc.dataCache,
}
vc.mu.Lock()
if vc.flowMap != nil {
@@ -286,6 +289,7 @@
writer: writer,
localEndpoint: vc.localEP,
remoteEndpoint: vc.remoteEP,
+ dataCache: vc.dataCache,
}
if err = vc.listener.Enqueue(f); err != nil {
f.Shutdown()
diff --git a/services/identity/auditor/blessing_auditor_test.go b/services/identity/auditor/blessing_auditor_test.go
index 7970ae3..9e53889 100644
--- a/services/identity/auditor/blessing_auditor_test.go
+++ b/services/identity/auditor/blessing_auditor_test.go
@@ -11,8 +11,7 @@
)
func TestBlessingAuditor(t *testing.T) {
- db := &mockDatabase{}
- auditor, reader := &blessingAuditor{db}, &blessingLogReader{db}
+ auditor, reader := NewMockBlessingAuditor()
p, err := vsecurity.NewPrincipal()
if err != nil {
@@ -87,23 +86,6 @@
}
}
-type mockDatabase struct {
- NextEntry databaseEntry
-}
-
-func (db *mockDatabase) Insert(entry databaseEntry) error {
- db.NextEntry = entry
- return nil
-}
-func (db *mockDatabase) Query(email string) <-chan databaseEntry {
- c := make(chan databaseEntry)
- go func() {
- c <- db.NextEntry
- close(c)
- }()
- return c
-}
-
func newThirdPartyCaveat(t *testing.T, p security.Principal) security.ThirdPartyCaveat {
tp, err := security.NewPublicKeyCaveat(p.PublicKey(), "location", security.ThirdPartyRequirements{}, newCaveat(security.MethodCaveat("method")))
if err != nil {
diff --git a/services/identity/auditor/mock_auditor.go b/services/identity/auditor/mock_auditor.go
new file mode 100644
index 0000000..6fe6c72
--- /dev/null
+++ b/services/identity/auditor/mock_auditor.go
@@ -0,0 +1,32 @@
+package auditor
+
+import (
+ "reflect"
+ "veyron.io/veyron/veyron/security/audit"
+)
+
+func NewMockBlessingAuditor() (audit.Auditor, BlessingLogReader) {
+ db := &mockDatabase{}
+ return &blessingAuditor{db}, &blessingLogReader{db}
+}
+
+type mockDatabase struct {
+ NextEntry databaseEntry
+}
+
+func (db *mockDatabase) Insert(entry databaseEntry) error {
+ db.NextEntry = entry
+ return nil
+}
+
+func (db *mockDatabase) Query(email string) <-chan databaseEntry {
+ c := make(chan databaseEntry)
+ go func() {
+ var empty databaseEntry
+ if !reflect.DeepEqual(db.NextEntry, empty) {
+ c <- db.NextEntry
+ }
+ close(c)
+ }()
+ return c
+}
diff --git a/services/identity/caveats/browser_caveat_selector.go b/services/identity/caveats/browser_caveat_selector.go
new file mode 100644
index 0000000..cb2ab4f
--- /dev/null
+++ b/services/identity/caveats/browser_caveat_selector.go
@@ -0,0 +1,226 @@
+package caveats
+
+import (
+ "fmt"
+ "html/template"
+ "net/http"
+ "strings"
+ "time"
+)
+
+type browserCaveatSelector struct{}
+
+// NewBrowserCaveatSelector returns a caveat selector that renders a form in the
+// to accept user caveat selections.
+func NewBrowserCaveatSelector() CaveatSelector {
+ return &browserCaveatSelector{}
+}
+
+func (s *browserCaveatSelector) Render(blessingExtension, state, redirectURL string, w http.ResponseWriter, r *http.Request) error {
+ tmplargs := struct {
+ Extension string
+ CaveatList []string
+ Macaroon, MacaroonURL string
+ }{blessingExtension, []string{"ExpiryCaveat", "MethodCaveat"}, state, redirectURL}
+ w.Header().Set("Context-Type", "text/html")
+ if err := tmplSelectCaveats.Execute(w, tmplargs); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (s *browserCaveatSelector) ParseSelections(r *http.Request) (caveats []CaveatInfo, state string, additionalExtension string, err error) {
+ if caveats, err = s.caveats(r); err != nil {
+ return
+ }
+ state = r.FormValue("macaroon")
+ additionalExtension = r.FormValue("blessingExtension")
+ return
+}
+
+func (s *browserCaveatSelector) caveats(r *http.Request) ([]CaveatInfo, error) {
+ if err := r.ParseForm(); err != nil {
+ return nil, err
+ }
+ var caveats []CaveatInfo
+ // Fill in the required caveat.
+ switch required := r.FormValue("requiredCaveat"); required {
+ case "Expiry":
+ expiry, err := newExpiryCaveatInfo(r.FormValue("expiry"), r.FormValue("timezoneOffset"))
+ if err != nil {
+ return nil, fmt.Errorf("failed to create ExpiryCaveat: %v", err)
+ }
+ caveats = append(caveats, expiry)
+ case "Revocation":
+ revocation := newRevocationCaveatInfo()
+ caveats = append(caveats, revocation)
+ default:
+ return nil, fmt.Errorf("%q is not a valid required caveat", required)
+ }
+ if len(caveats) != 1 {
+ return nil, fmt.Errorf("server does not allow for un-restricted blessings")
+ }
+
+ // And find any additional ones
+ for i, cavName := range r.Form["caveat"] {
+ var err error
+ var caveat CaveatInfo
+ switch cavName {
+ case "ExpiryCaveat":
+ if caveat, err = newExpiryCaveatInfo(r.Form[cavName][i], r.FormValue("timezoneOffset")); err != nil {
+ return nil, fmt.Errorf("unable to create caveat %s: %v", cavName, err)
+ }
+ case "MethodCaveat":
+ if caveat, err = newMethodCaveatInfo(strings.Split(r.Form[cavName][i], ",")); err != nil {
+ return nil, fmt.Errorf("unable to create caveat %s: %v", cavName, err)
+ }
+ case "none":
+ continue
+ default:
+ return nil, fmt.Errorf("unable to create caveat %s: caveat does not exist", cavName)
+ }
+ caveats = append(caveats, caveat)
+ }
+ return caveats, nil
+}
+
+func newExpiryCaveatInfo(timestamp, utcOffset string) (CaveatInfo, error) {
+ var empty CaveatInfo
+ t, err := time.Parse("2006-01-02T15:04", timestamp)
+ if err != nil {
+ return empty, fmt.Errorf("parseTime failed: %v", err)
+ }
+ // utcOffset is returned as minutes from JS, so we need to parse it to a duration.
+ offset, err := time.ParseDuration(utcOffset + "m")
+ if err != nil {
+ return empty, fmt.Errorf("failed to parse duration: %v", err)
+ }
+ return CaveatInfo{"Expiry", []interface{}{t.Add(offset)}}, nil
+}
+
+func newMethodCaveatInfo(methods []string) (CaveatInfo, error) {
+ if len(methods) < 1 {
+ return CaveatInfo{}, fmt.Errorf("must pass at least one method")
+ }
+ var ifaces []interface{}
+ for _, m := range methods {
+ ifaces = append(ifaces, m)
+ }
+ return CaveatInfo{"Method", ifaces}, nil
+}
+
+func newRevocationCaveatInfo() CaveatInfo {
+ return CaveatInfo{Type: "Revocation"}
+}
+
+var tmplSelectCaveats = template.Must(template.New("bless").Parse(`<!doctype html>
+<html>
+<head>
+<meta charset="UTF-8">
+<title>Blessings: Select caveats</title>
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css">
+<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/css/toastr.min.css">
+<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.7.0/moment.min.js"></script>
+<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
+<script src="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/js/toastr.min.js"></script>
+<script>
+ // TODO(suharshs): Move this and other JS/CSS to an assets directory in identity server.
+ $(document).ready(function() {
+ $('.caveatInput').hide(); // Hide all the inputs at start.
+
+ // When a caveat selector changes show the corresponding input box.
+ $('body').on('change', '.caveats', function (){
+ // Grab the div encapsulating the select and the corresponding inputs.
+ var caveatSelector = $(this).parents(".caveatRow");
+ // Hide the visible inputs and show the selected one.
+ caveatSelector.find('.caveatInput').hide();
+ caveatSelector.find('#'+$(this).val()).show();
+ });
+
+ // Upon clicking the '+' button a new caveat selector should appear.
+ $('body').on('click', '.addCaveat', function() {
+ var selector = $(this).parents(".caveatRow");
+ var newSelector = selector.clone();
+ // Hide all inputs since nothing is selected in this clone.
+ newSelector.find('.caveatInput').hide();
+ selector.after(newSelector);
+ // Change the '+' button to a '-' button.
+ $(this).replaceWith('<button type="button" class="btn btn-danger btn-sm removeCaveat">-</button>')
+ });
+
+ // Upon clicking the '-' button caveats should be removed.
+ $('body').on('click', '.removeCaveat', function() {
+ $(this).parents('.caveatRow').remove();
+ });
+
+ // Get the timezoneOffset for the server to create a correct expiry caveat.
+ // The offset is the minutes between UTC and local time.
+ var d = new Date();
+ $('#timezoneOffset').val(d.getTimezoneOffset());
+
+ // Set the datetime picker to have a default value of one day from now.
+ var m = moment().add(1, 'd').format("YYYY-MM-DDTHH:MM")
+ $('#expiry').val(m);
+ $('#ExpiryCaveat').val(m);
+ });
+</script>
+</head>
+<body class="container">
+<form class="form-horizontal" method="POST" id="caveats-form" name="input" action="{{.MacaroonURL}}" role="form">
+<h2 class="form-signin-heading">{{.Extension}}</h2>
+<input type="text" class="hidden" name="macaroon" value="{{.Macaroon}}">
+<div class="form-group form-group-lg">
+ <label class="col-sm-2" for="blessing-extension">Extension</label>
+ <div class="col-sm-10">
+ <input name="blessingExtension" type="text" class="form-control" id="blessing-extension" placeholder="(optional) name of the device/application for which the blessing is being sought, e.g. homelaptop">
+ <input type="text" class="hidden" id="timezoneOffset" name="timezoneOffset">
+ </div>
+</div>
+<div class="form-group form-group-lg">
+ <label class="col-sm-2" for="required-caveat">Expiration</label>
+ <div class="col-sm-10" class="input-group" name="required-caveat">
+ <div class="radio">
+ <label>
+ <input type="radio" name="requiredCaveat" id="requiredCaveat" value="Revocation" checked>
+ When explicitly revoked
+ </label>
+ </div>
+ <div class="radio">
+ <div class="input-group">
+ <input type="radio" name="requiredCaveat" id="requiredCaveat" value="Expiry">
+ <input type="datetime-local" id="expiry" name="expiry">
+ </div>
+ </div>
+ </div>
+</div>
+<h4 class="form-signin-heading">Additional caveats</h4>
+<span class="help-text">Optional additional restrictions on the use of the blessing</span>
+<div class="caveatRow row">
+ <div class="col-md-4">
+ <select name="caveat" class="form-control caveats">
+ <option value="none" selected="selected">Select a caveat.</option>
+ {{ $caveatList := .CaveatList }}
+ {{range $index, $name := $caveatList}}
+ <option name="{{$name}}" value="{{$name}}">{{$name}}</option>
+ {{end}}
+ </select>
+ </div>
+ <div class="col-md-7">
+ {{range $index, $name := $caveatList}}
+ {{if eq $name "ExpiryCaveat"}}
+ <input type="datetime-local" class="form-control caveatInput" id="{{$name}}" name="{{$name}}">
+ {{else if eq $name "MethodCaveat"}}
+ <input type="text" id="{{$name}}" class="form-control caveatInput" name="{{$name}}" placeholder="comma-separated method list">
+ {{end}}
+ {{end}}
+ </div>
+ <div class="col-md-1">
+ <button type="button" class="btn btn-info btn-sm addCaveat">+</button>
+ </div>
+</div>
+<br/>
+<button class="btn btn-lg btn-primary btn-block" type="submit">Bless</button>
+</form>
+</body>
+</html>`))
diff --git a/services/identity/caveats/caveat_factory.go b/services/identity/caveats/caveat_factory.go
new file mode 100644
index 0000000..3797ca7
--- /dev/null
+++ b/services/identity/caveats/caveat_factory.go
@@ -0,0 +1,92 @@
+package caveats
+
+import (
+ "fmt"
+ "time"
+
+ "veyron.io/veyron/veyron/services/identity/revocation"
+
+ "veyron.io/veyron/veyron2/security"
+)
+
+type CaveatFactory interface {
+ New(caveatInfo CaveatInfo) (security.Caveat, error)
+}
+
+type CaveatInfo struct {
+ Type string
+ Args []interface{}
+}
+
+type caveatFactory map[string]func(args ...interface{}) (security.Caveat, error)
+
+func NewCaveatFactory() CaveatFactory {
+ return caveatFactory{
+ "Expiry": expiryCaveat,
+ "Method": methodCaveat,
+ "Revocation": revocationCaveat,
+ }
+}
+
+func (c caveatFactory) New(caveatInfo CaveatInfo) (security.Caveat, error) {
+ fact, exists := c[caveatInfo.Type]
+ if !exists {
+ return security.Caveat{}, fmt.Errorf("caveat %s does not exist in CaveatFactory", caveatInfo.Type)
+ }
+ return fact(caveatInfo.Args...)
+}
+
+func expiryCaveat(args ...interface{}) (security.Caveat, error) {
+ var empty security.Caveat
+ if len(args) != 1 {
+ return empty, fmt.Errorf("expiry caveat: must input exactly one time argument")
+ }
+ t, ok := args[0].(time.Time)
+ if !ok {
+ return empty, fmt.Errorf("expiry caveat: received arg of type %T, expected time.Time", args[0])
+ }
+ return security.ExpiryCaveat(t)
+}
+
+func methodCaveat(args ...interface{}) (security.Caveat, error) {
+ var empty security.Caveat
+ if len(args) < 1 {
+ return empty, fmt.Errorf("method caveat requires at least one argument")
+ }
+ methods, err := interfacesToStrings(args)
+ if err != nil {
+ return empty, fmt.Errorf("method caveat: %v", err)
+ }
+ return security.MethodCaveat(methods[0], methods[1:]...)
+}
+
+func interfacesToStrings(args []interface{}) (s []string, err error) {
+ for _, arg := range args {
+ a, ok := arg.(string)
+ if !ok {
+ return nil, fmt.Errorf("received arg of type %T, expected string", arg)
+ }
+ s = append(s, a)
+ }
+ return s, nil
+}
+
+func revocationCaveat(args ...interface{}) (security.Caveat, error) {
+ var empty security.Caveat
+ if len(args) != 3 {
+ return empty, fmt.Errorf("revocation caveat: must input a revocation manager, publickey, and discharge location")
+ }
+ revocationManager, ok := args[0].(revocation.RevocationManager)
+ if !ok {
+ return empty, fmt.Errorf("revocation caveat: received args of type %T, expected revocation.RevocationManager", args[0])
+ }
+ publicKey, ok := args[1].(security.PublicKey)
+ if !ok {
+ return empty, fmt.Errorf("revocation caveat: received args of type %T, expected security.PublicKey", args[1])
+ }
+ dischargerLocation, ok := args[2].(string)
+ if !ok {
+ return empty, fmt.Errorf("revocation caveat: received args of type %T, expected string", args[2])
+ }
+ return revocationManager.NewCaveat(publicKey, dischargerLocation)
+}
diff --git a/services/identity/caveats/caveat_selector.go b/services/identity/caveats/caveat_selector.go
new file mode 100644
index 0000000..8876a55
--- /dev/null
+++ b/services/identity/caveats/caveat_selector.go
@@ -0,0 +1,19 @@
+package caveats
+
+import (
+ "net/http"
+)
+
+// CaveatSelector is used to render a web page where the user can select caveats
+// to be added to a blessing being granted
+type CaveatSelector interface {
+ // Render renders the caveat input form. When the user has completed inputing caveats,
+ // Render should redirect to the specified redirect route.
+ // blessingExtension is the extension used for the blessings that is being caveated.
+ // state is any state passed by the caller (e.g., for CSRF mitigation) and is returned by ParseSelections.
+ // redirectRoute is the route to be returned to.
+ Render(blessingExtension, state, redirectURL string, w http.ResponseWriter, r *http.Request) error
+ // ParseSelections parse the users choices of Caveats, and returns the information needed to create them,
+ // the state passed to Render, and any additionalExtension selected by the user to further extend the blessing.
+ ParseSelections(r *http.Request) (caveats []CaveatInfo, state string, additionalExtension string, err error)
+}
diff --git a/services/identity/caveats/mock_caveat_selector.go b/services/identity/caveats/mock_caveat_selector.go
new file mode 100644
index 0000000..6c20637
--- /dev/null
+++ b/services/identity/caveats/mock_caveat_selector.go
@@ -0,0 +1,35 @@
+package caveats
+
+import (
+ "net/http"
+ "time"
+)
+
+type mockCaveatSelector struct {
+ state string
+}
+
+// NewMockCaveatSelector returns a CaveatSelector that always returns a default set
+// of caveats: [exprity caveat with a 1h expiry, revocation caveat, and a method caveat
+// for methods "methodA" and "methodB"] and the additional extension: "test-extension"
+// This selector is only meant to be used during testing.
+func NewMockCaveatSelector() CaveatSelector {
+ return &mockCaveatSelector{}
+}
+
+func (s *mockCaveatSelector) Render(_, state, redirectURL string, w http.ResponseWriter, r *http.Request) error {
+ s.state = state
+ http.Redirect(w, r, redirectURL, http.StatusFound)
+ return nil
+}
+
+func (s *mockCaveatSelector) ParseSelections(r *http.Request) (caveats []CaveatInfo, state string, additionalExtension string, err error) {
+ caveats = []CaveatInfo{
+ CaveatInfo{"Revocation", []interface{}{}},
+ CaveatInfo{"Expiry", []interface{}{time.Now().Add(time.Hour)}},
+ CaveatInfo{"Method", []interface{}{"methodA", "methodB"}},
+ }
+ state = s.state
+ additionalExtension = "test-extension"
+ return
+}
diff --git a/services/identity/identityd/main.go b/services/identity/identityd/main.go
index c1c9655..c18e7ab 100644
--- a/services/identity/identityd/main.go
+++ b/services/identity/identityd/main.go
@@ -16,6 +16,7 @@
"veyron.io/veyron/veyron/services/identity/auditor"
"veyron.io/veyron/veyron/services/identity/blesser"
+ "veyron.io/veyron/veyron/services/identity/caveats"
"veyron.io/veyron/veyron/services/identity/oauth"
"veyron.io/veyron/veyron/services/identity/revocation"
"veyron.io/veyron/veyron/services/identity/server"
@@ -64,7 +65,13 @@
vlog.Fatalf("Failed to start RevocationManager: %v", err)
}
- server.NewIdentityServer(googleoauth, auditor, reader, revocationManager, oauthBlesserGoogleParams(revocationManager)).Serve()
+ server.NewIdentityServer(
+ googleoauth,
+ auditor,
+ reader,
+ revocationManager,
+ oauthBlesserGoogleParams(revocationManager),
+ caveats.NewBrowserCaveatSelector()).Serve()
}
func usage() {
diff --git a/services/identity/identityd_test/main.go b/services/identity/identityd_test/main.go
new file mode 100644
index 0000000..371ff64
--- /dev/null
+++ b/services/identity/identityd_test/main.go
@@ -0,0 +1,58 @@
+// HTTP server that uses OAuth to create security.Blessings objects.
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+ "time"
+
+ "veyron.io/veyron/veyron/services/identity/auditor"
+ "veyron.io/veyron/veyron/services/identity/blesser"
+ "veyron.io/veyron/veyron/services/identity/caveats"
+ "veyron.io/veyron/veyron/services/identity/oauth"
+ "veyron.io/veyron/veyron/services/identity/revocation"
+ "veyron.io/veyron/veyron/services/identity/server"
+)
+
+var (
+ googleDomain = flag.String("google_domain", "", "An optional domain name. When set, only email addresses from this domain are allowed to authenticate via Google OAuth")
+)
+
+func main() {
+ flag.Usage = usage
+ flag.Parse()
+
+ auditor, reader := auditor.NewMockBlessingAuditor()
+ revocationManager := revocation.NewMockRevocationManager()
+
+ server.NewIdentityServer(
+ oauth.NewMockOAuth(),
+ auditor,
+ reader,
+ revocationManager,
+ oauthBlesserGoogleParams(revocationManager),
+ caveats.NewMockCaveatSelector()).Serve()
+}
+
+func usage() {
+ fmt.Fprintf(os.Stderr, `%s starts an test version of the identityd server that
+mocks out oauth, auditing, and revocation.
+
+To generate TLS certificates so the HTTP server can use SSL:
+go run $GOROOT/src/pkg/crypto/tls/generate_cert.go --host <IP address>
+
+Flags:
+`, os.Args[0])
+ flag.PrintDefaults()
+}
+
+func oauthBlesserGoogleParams(revocationManager revocation.RevocationManager) blesser.GoogleParams {
+ googleParams := blesser.GoogleParams{
+ BlessingDuration: 365 * 24 * time.Hour,
+ DomainRestriction: *googleDomain,
+ RevocationManager: revocationManager,
+ }
+ // TODO(suharshs): Figure out the test for this.
+ return googleParams
+}
diff --git a/services/identity/oauth/handler.go b/services/identity/oauth/handler.go
index b85e16d..5ad758a 100644
--- a/services/identity/oauth/handler.go
+++ b/services/identity/oauth/handler.go
@@ -33,6 +33,7 @@
"veyron.io/veyron/veyron/services/identity/auditor"
"veyron.io/veyron/veyron/services/identity/blesser"
+ "veyron.io/veyron/veyron/services/identity/caveats"
"veyron.io/veyron/veyron/services/identity/revocation"
"veyron.io/veyron/veyron/services/identity/util"
"veyron.io/veyron/veyron2"
@@ -76,6 +77,8 @@
DomainRestriction string
// OAuthProvider is used to authenticate and get a blessee email.
OAuthProvider OAuthProvider
+ // CaveatSelector is used to obtain caveats from the user when seeking a blessing.
+ CaveatSelector caveats.CaveatSelector
}
func redirectURL(baseURL, suffix string) string {
@@ -300,8 +303,6 @@
ToolRedirectURL, ToolState, Email string
}
-var caveatList = []string{"ExpiryCaveat", "MethodCaveat"}
-
func (h *handler) addCaveats(w http.ResponseWriter, r *http.Request) {
var inputMacaroon seekBlessingsMacaroon
if err := h.csrfCop.ValidateToken(r.FormValue("state"), r, clientIDCookie, &inputMacaroon); err != nil {
@@ -327,34 +328,33 @@
util.HTTPServerError(w, fmt.Errorf("failed to create new token: %v", err))
return
}
- tmplargs := struct {
- Extension string
- CaveatList []string
- Macaroon, MacaroonRoute string
- }{email, caveatList, outputMacaroon, sendMacaroonRoute}
- w.Header().Set("Context-Type", "text/html")
- if err := tmplSelectCaveats.Execute(w, tmplargs); err != nil {
- vlog.Errorf("Unable to execute bless page template: %v", err)
+ if err := h.args.CaveatSelector.Render(email, outputMacaroon, redirectURL(h.args.Addr, sendMacaroonRoute), w, r); err != nil {
+ vlog.Errorf("Unable to invoke render caveat selector: %v", err)
util.HTTPServerError(w, err)
}
}
func (h *handler) sendMacaroon(w http.ResponseWriter, r *http.Request) {
var inputMacaroon addCaveatsMacaroon
- if err := h.csrfCop.ValidateToken(r.FormValue("macaroon"), r, clientIDCookie, &inputMacaroon); err != nil {
- util.HTTPBadRequest(w, r, fmt.Errorf("Suspected request forgery: %v", err))
+ caveatInfos, macaroonString, blessingExtension, err := h.args.CaveatSelector.ParseSelections(r)
+ if err != nil {
+ util.HTTPBadRequest(w, r, fmt.Errorf("failed to parse blessing information: %v", err))
return
}
- blessingExtension := r.FormValue("blessingExtension")
+ if err := h.csrfCop.ValidateToken(macaroonString, r, clientIDCookie, &inputMacaroon); err != nil {
+ util.HTTPBadRequest(w, r, fmt.Errorf("suspected request forgery: %v", err))
+ return
+ }
+
+ caveats, err := h.caveats(caveatInfos)
+ if err != nil {
+ util.HTTPBadRequest(w, r, fmt.Errorf("failed to create caveats: %v", err))
+ return
+ }
name := inputMacaroon.Email
if len(blessingExtension) > 0 {
name = name + security.ChainSeparator + blessingExtension
}
- caveats, err := h.caveats(r)
- if err != nil {
- util.HTTPBadRequest(w, r, fmt.Errorf("failed to extract caveats: ", err))
- return
- }
if len(caveats) == 0 {
util.HTTPBadRequest(w, r, fmt.Errorf("server disallows attempts to bless with no caveats"))
return
@@ -366,13 +366,13 @@
Name: name,
}
if err := vom.NewEncoder(buf).Encode(m); err != nil {
- util.HTTPServerError(w, fmt.Errorf("failed to encode BlessingsMacaroon: ", err))
+ util.HTTPServerError(w, fmt.Errorf("failed to encode BlessingsMacaroon: %v", err))
return
}
// Construct the url to send back to the tool.
baseURL, err := validLoopbackURL(inputMacaroon.ToolRedirectURL)
if err != nil {
- util.HTTPBadRequest(w, r, fmt.Errorf("invalid ToolRedirectURL: ", err))
+ util.HTTPBadRequest(w, r, fmt.Errorf("invalid ToolRedirectURL: %v", err))
return
}
params := url.Values{}
@@ -383,74 +383,17 @@
http.Redirect(w, r, baseURL.String(), http.StatusFound)
}
-func (h *handler) caveats(r *http.Request) ([]security.Caveat, error) {
- if err := r.ParseForm(); err != nil {
- return nil, err
- }
- var caveats []security.Caveat
- // Fill in the required caveat.
- switch required := r.FormValue("requiredCaveat"); required {
- case "Expiry":
- expiry, err := newExpiryCaveat(r.FormValue("expiry"), r.FormValue("timezoneOffset"))
+func (h *handler) caveats(caveatInfos []caveats.CaveatInfo) (cavs []security.Caveat, err error) {
+ caveatFactories := caveats.NewCaveatFactory()
+ for _, caveatInfo := range caveatInfos {
+ if caveatInfo.Type == "Revocation" {
+ caveatInfo.Args = []interface{}{h.args.RevocationManager, h.args.R.Principal().PublicKey(), h.args.DischargerLocation}
+ }
+ cav, err := caveatFactories.New(caveatInfo)
if err != nil {
- return nil, fmt.Errorf("failed to create ExpiryCaveat: %v", err)
+ return nil, err
}
- caveats = append(caveats, expiry)
- case "Revocation":
- if h.args.RevocationManager == nil {
- return nil, fmt.Errorf("server not configured to support revocation")
- }
- revocation, err := h.args.RevocationManager.NewCaveat(h.args.R.Principal().PublicKey(), h.args.DischargerLocation)
- if err != nil {
- return nil, fmt.Errorf("failed to create revocation caveat: %v", err)
- }
- caveats = append(caveats, revocation)
- default:
- return nil, fmt.Errorf("%q is not a valid required caveat", required)
+ cavs = append(cavs, cav)
}
- if len(caveats) != 1 {
- return nil, fmt.Errorf("server does not allow for un-restricted blessings")
- }
-
- // And find any additional ones
- for i, cavName := range r.Form["caveat"] {
- var err error
- var caveat security.Caveat
- switch cavName {
- case "ExpiryCaveat":
- caveat, err = newExpiryCaveat(r.Form[cavName][i], r.FormValue("timezoneOffset"))
- case "MethodCaveat":
- caveat, err = newMethodCaveat(strings.Split(r.Form[cavName][i], ","))
- case "none":
- continue
- default:
- return nil, fmt.Errorf("unable to create caveat %s: caveat does not exist", cavName)
- }
- if err != nil {
- return nil, fmt.Errorf("unable to create caveat %s: %v", cavName, err)
- }
- caveats = append(caveats, caveat)
- }
- return caveats, nil
-}
-
-func newExpiryCaveat(timestamp, utcOffset string) (security.Caveat, error) {
- var empty security.Caveat
- t, err := time.Parse("2006-01-02T15:04", timestamp)
- if err != nil {
- return empty, fmt.Errorf("parseTime failed: %v", err)
- }
- // utcOffset is returned as minutes from JS, so we need to parse it to a duration.
- offset, err := time.ParseDuration(utcOffset + "m")
- if err != nil {
- return empty, fmt.Errorf("failed to parse duration: %v", err)
- }
- return security.ExpiryCaveat(t.Add(offset))
-}
-
-func newMethodCaveat(methods []string) (security.Caveat, error) {
- if len(methods) < 1 {
- return security.Caveat{}, fmt.Errorf("must pass at least one method")
- }
- return security.MethodCaveat(methods[0], methods[1:]...)
+ return
}
diff --git a/services/identity/oauth/mockoauth.go b/services/identity/oauth/mockoauth.go
new file mode 100644
index 0000000..8fa1806
--- /dev/null
+++ b/services/identity/oauth/mockoauth.go
@@ -0,0 +1,16 @@
+package oauth
+
+// mockOAuth is a mock OAuthProvider for use in tests.
+type mockOAuth struct{}
+
+func NewMockOAuth() OAuthProvider {
+ return &mockOAuth{}
+}
+
+func (m *mockOAuth) AuthURL(redirectUrl string, state string) string {
+ return redirectUrl + "?state=" + state
+}
+
+func (m *mockOAuth) ExchangeAuthCodeForEmail(authCode string, url string) (email string, err error) {
+ return "testemail@google.com", nil
+}
diff --git a/services/identity/oauth/template.go b/services/identity/oauth/template.go
deleted file mode 100644
index 698635a..0000000
--- a/services/identity/oauth/template.go
+++ /dev/null
@@ -1,230 +0,0 @@
-package oauth
-
-import "html/template"
-
-var tmplViewBlessings = template.Must(template.New("auditor").Parse(`<!doctype html>
-<html>
-<head>
-<meta charset="UTF-8">
-<title>Blessings for {{.Email}}</title>
-<meta name="viewport" content="width=device-width, initial-scale=1.0">
-<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
-<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/css/toastr.min.css">
-<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.7.0/moment.min.js"></script>
-<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
-<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.11.0/jquery-ui.min.js"></script>
-<script src="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/js/toastr.min.js"></script>
-<script>
-function setTimeText(elem) {
- var timestamp = elem.data("unixtime");
- var m = moment(timestamp*1000.0);
- var style = elem.data("style");
- if (style === "absolute") {
- elem.html("<a href='#'>" + m.format("dd, MMM Do YYYY, h:mm:ss a") + "</a>");
- elem.data("style", "fromNow");
- } else {
- elem.html("<a href='#'>" + m.fromNow() + "</a>");
- elem.data("style", "absolute");
- }
-}
-
-$(document).ready(function() {
- $(".unixtime").each(function() {
- // clicking the timestamp should toggle the display format.
- $(this).click(function() { setTimeText($(this)); });
- setTimeText($(this));
- });
-
- // Setup the revoke buttons click events.
- $(".revoke").click(function() {
- var revokeButton = $(this);
- $.ajax({
- url: "/google/{{.RevokeRoute}}",
- type: "POST",
- data: JSON.stringify({
- "Token": revokeButton.val()
- })
- }).done(function(data) {
- if (data.success == "false") {
- failMessage(revokeButton);
- return;
- }
- revokeButton.replaceWith("<div>Just Revoked!</div>");
- }).fail(function(xhr, textStatus){
- failMessage(revokeButton);
- console.error('Bad request: %s', status, xhr)
- });
- });
-});
-
-function failMessage(revokeButton) {
- revokeButton.parent().parent().fadeIn(function(){
- $(this).addClass("bg-danger");
- });
- toastr.options.closeButton = true;
- toastr.error('Unable to revoke identity!', 'Error!')
-}
-
-</script>
-</head>
-<body>
-<div class="container">
-<h3>Blessing log for {{.Email}}</h3>
-<table class="table table-bordered table-hover table-responsive">
-<thead>
- <tr>
- <th>Blessed as</th>
- <th>Public Key</th>
- <th>Issued</th>
- <th>Caveats</th>
- <th>Revoked</th>
- </tr>
-</thead>
-<tbody>
-{{range .Log}}
- {{if .Error}}
- <tr class="bg-danger">
- <td colspan="5">Failed to read audit log: Error: {{.Error}}</td>
- </tr>
- {{else}}
- <tr>
- <td>{{.Blessed}}</td>
- <td>{{.Blessed.PublicKey}}</td>
- <td><div class="unixtime" data-unixtime={{.Timestamp.Unix}}>{{.Timestamp.String}}</div></td>
- <td>
- {{range .Caveats}}
- {{.}}</br>
- {{end}}
- </td>
- <td>
- {{ if .Token }}
- <button class="revoke" value="{{.Token}}">Revoke</button>
- {{ else if not .RevocationTime.IsZero }}
- <div class="unixtime" data-unixtime={{.RevocationTime.Unix}}>{{.RevocationTime.String}}</div>
- {{ end }}
- </td>
- </tr>
- {{end}}
-{{else}}
- <tr>
- <td colspan=5>No blessings issued</td>
- </tr>
-{{end}}
-</tbody>
-</table>
-<hr/>
-</div>
-</body>
-</html>`))
-
-var tmplSelectCaveats = template.Must(template.New("bless").Parse(`<!doctype html>
-<html>
-<head>
-<meta charset="UTF-8">
-<title>Blessings: Select caveats</title>
-<meta name="viewport" content="width=device-width, initial-scale=1.0">
-<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css">
-<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/css/toastr.min.css">
-<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.7.0/moment.min.js"></script>
-<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
-<script src="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/js/toastr.min.js"></script>
-<script>
- // TODO(suharshs): Move this and other JS/CSS to an assets directory in identity server.
- $(document).ready(function() {
- $('.caveatInput').hide(); // Hide all the inputs at start.
-
- // When a caveat selector changes show the corresponding input box.
- $('body').on('change', '.caveats', function (){
- // Grab the div encapsulating the select and the corresponding inputs.
- var caveatSelector = $(this).parents(".caveatRow");
- // Hide the visible inputs and show the selected one.
- caveatSelector.find('.caveatInput').hide();
- caveatSelector.find('#'+$(this).val()).show();
- });
-
- // Upon clicking the '+' button a new caveat selector should appear.
- $('body').on('click', '.addCaveat', function() {
- var selector = $(this).parents(".caveatRow");
- var newSelector = selector.clone();
- // Hide all inputs since nothing is selected in this clone.
- newSelector.find('.caveatInput').hide();
- selector.after(newSelector);
- // Change the '+' button to a '-' button.
- $(this).replaceWith('<button type="button" class="btn btn-danger btn-sm removeCaveat">-</button>')
- });
-
- // Upon clicking the '-' button caveats should be removed.
- $('body').on('click', '.removeCaveat', function() {
- $(this).parents('.caveatRow').remove();
- });
-
- // Get the timezoneOffset for the server to create a correct expiry caveat.
- // The offset is the minutes between UTC and local time.
- var d = new Date();
- $('#timezoneOffset').val(d.getTimezoneOffset());
-
- // Set the datetime picker to have a default value of one day from now.
- var m = moment().add(1, 'd').format("YYYY-MM-DDTHH:MM")
- $('#expiry').val(m);
- $('#ExpiryCaveat').val(m);
- });
-</script>
-</head>
-<body class="container">
-<form class="form-horizontal" method="POST" id="caveats-form" name="input" action="/google/{{.MacaroonRoute}}" role="form">
-<h2 class="form-signin-heading">{{.Extension}}</h2>
-<input type="text" class="hidden" name="macaroon" value="{{.Macaroon}}">
-<div class="form-group form-group-lg">
- <label class="col-sm-2" for="blessing-extension">Extension</label>
- <div class="col-sm-10">
- <input name="blessingExtension" type="text" class="form-control" id="blessing-extension" placeholder="(optional) name of the device/application for which the blessing is being sought, e.g. homelaptop">
- <input type="text" class="hidden" id="timezoneOffset" name="timezoneOffset">
- </div>
-</div>
-<div class="form-group form-group-lg">
- <label class="col-sm-2" for="required-caveat">Expiration</label>
- <div class="col-sm-10" class="input-group" name="required-caveat">
- <div class="radio">
- <label>
- <input type="radio" name="requiredCaveat" id="requiredCaveat" value="Revocation" checked>
- When explicitly revoked
- </label>
- </div>
- <div class="radio">
- <div class="input-group">
- <input type="radio" name="requiredCaveat" id="requiredCaveat" value="Expiry">
- <input type="datetime-local" id="expiry" name="expiry">
- </div>
- </div>
- </div>
-</div>
-<h4 class="form-signin-heading">Additional caveats</h4>
-<span class="help-text">Optional additional restrictions on the use of the blessing</span>
-<div class="caveatRow row">
- <div class="col-md-4">
- <select name="caveat" class="form-control caveats">
- <option value="none" selected="selected">Select a caveat.</option>
- {{ $caveatList := .CaveatList }}
- {{range $index, $name := $caveatList}}
- <option name="{{$name}}" value="{{$name}}">{{$name}}</option>
- {{end}}
- </select>
- </div>
- <div class="col-md-7">
- {{range $index, $name := $caveatList}}
- {{if eq $name "ExpiryCaveat"}}
- <input type="datetime-local" class="form-control caveatInput" id="{{$name}}" name="{{$name}}">
- {{else if eq $name "MethodCaveat"}}
- <input type="text" id="{{$name}}" class="form-control caveatInput" name="{{$name}}" placeholder="comma-separated method list">
- {{end}}
- {{end}}
- </div>
- <div class="col-md-1">
- <button type="button" class="btn btn-info btn-sm addCaveat">+</button>
- </div>
-</div>
-<br/>
-<button class="btn btn-lg btn-primary btn-block" type="submit">Bless</button>
-</form>
-</body>
-</html>`))
diff --git a/services/identity/oauth/view_blessings_template.go b/services/identity/oauth/view_blessings_template.go
new file mode 100644
index 0000000..66334a7
--- /dev/null
+++ b/services/identity/oauth/view_blessings_template.go
@@ -0,0 +1,118 @@
+package oauth
+
+import "html/template"
+
+var tmplViewBlessings = template.Must(template.New("auditor").Parse(`<!doctype html>
+<html>
+<head>
+<meta charset="UTF-8">
+<title>Blessings for {{.Email}}</title>
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
+<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/css/toastr.min.css">
+<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.7.0/moment.min.js"></script>
+<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
+<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.11.0/jquery-ui.min.js"></script>
+<script src="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/js/toastr.min.js"></script>
+<script>
+function setTimeText(elem) {
+ var timestamp = elem.data("unixtime");
+ var m = moment(timestamp*1000.0);
+ var style = elem.data("style");
+ if (style === "absolute") {
+ elem.html("<a href='#'>" + m.format("dd, MMM Do YYYY, h:mm:ss a") + "</a>");
+ elem.data("style", "fromNow");
+ } else {
+ elem.html("<a href='#'>" + m.fromNow() + "</a>");
+ elem.data("style", "absolute");
+ }
+}
+
+$(document).ready(function() {
+ $(".unixtime").each(function() {
+ // clicking the timestamp should toggle the display format.
+ $(this).click(function() { setTimeText($(this)); });
+ setTimeText($(this));
+ });
+
+ // Setup the revoke buttons click events.
+ $(".revoke").click(function() {
+ var revokeButton = $(this);
+ $.ajax({
+ url: "/google/{{.RevokeRoute}}",
+ type: "POST",
+ data: JSON.stringify({
+ "Token": revokeButton.val()
+ })
+ }).done(function(data) {
+ if (data.success == "false") {
+ failMessage(revokeButton);
+ return;
+ }
+ revokeButton.replaceWith("<div>Just Revoked!</div>");
+ }).fail(function(xhr, textStatus){
+ failMessage(revokeButton);
+ console.error('Bad request: %s', status, xhr)
+ });
+ });
+});
+
+function failMessage(revokeButton) {
+ revokeButton.parent().parent().fadeIn(function(){
+ $(this).addClass("bg-danger");
+ });
+ toastr.options.closeButton = true;
+ toastr.error('Unable to revoke identity!', 'Error!')
+}
+
+</script>
+</head>
+<body>
+<div class="container">
+<h3>Blessing log for {{.Email}}</h3>
+<table class="table table-bordered table-hover table-responsive">
+<thead>
+ <tr>
+ <th>Blessed as</th>
+ <th>Public Key</th>
+ <th>Issued</th>
+ <th>Caveats</th>
+ <th>Revoked</th>
+ </tr>
+</thead>
+<tbody>
+{{range .Log}}
+ {{if .Error}}
+ <tr class="bg-danger">
+ <td colspan="5">Failed to read audit log: Error: {{.Error}}</td>
+ </tr>
+ {{else}}
+ <tr>
+ <td>{{.Blessed}}</td>
+ <td>{{.Blessed.PublicKey}}</td>
+ <td><div class="unixtime" data-unixtime={{.Timestamp.Unix}}>{{.Timestamp.String}}</div></td>
+ <td>
+ {{range .Caveats}}
+ {{.}}</br>
+ {{end}}
+ </td>
+ <td>
+ {{ if .Token }}
+ <button class="revoke" value="{{.Token}}">Revoke</button>
+ {{ else if not .RevocationTime.IsZero }}
+ <div class="unixtime" data-unixtime={{.RevocationTime.Unix}}>{{.RevocationTime.String}}</div>
+ {{ end }}
+ </td>
+ </tr>
+ {{end}}
+{{else}}
+ <tr>
+ <td colspan=5>No blessings issued</td>
+ </tr>
+{{end}}
+</tbody>
+</table>
+<hr/>
+</div>
+</body>
+</html>`))
diff --git a/services/identity/revocation/mock_revocation_manager.go b/services/identity/revocation/mock_revocation_manager.go
new file mode 100644
index 0000000..560a553
--- /dev/null
+++ b/services/identity/revocation/mock_revocation_manager.go
@@ -0,0 +1,35 @@
+package revocation
+
+import (
+ "time"
+)
+
+func NewMockRevocationManager() RevocationManager {
+ revocationDB = &mockDatabase{make(map[string][]byte), make(map[string]*time.Time)}
+ return &revocationManager{}
+}
+
+type mockDatabase struct {
+ tpCavIDToRevCavID map[string][]byte
+ revCavIDToTimestamp map[string]*time.Time
+}
+
+func (m *mockDatabase) InsertCaveat(thirdPartyCaveatID string, revocationCaveatID []byte) error {
+ m.tpCavIDToRevCavID[thirdPartyCaveatID] = revocationCaveatID
+ return nil
+}
+
+func (m *mockDatabase) Revoke(thirdPartyCaveatID string) error {
+ timestamp := time.Now()
+ m.revCavIDToTimestamp[string(m.tpCavIDToRevCavID[thirdPartyCaveatID])] = ×tamp
+ return nil
+}
+
+func (m *mockDatabase) IsRevoked(revocationCaveatID []byte) (bool, error) {
+ _, exists := m.revCavIDToTimestamp[string(revocationCaveatID)]
+ return exists, nil
+}
+
+func (m *mockDatabase) RevocationTime(thirdPartyCaveatID string) (*time.Time, error) {
+ return m.revCavIDToTimestamp[string(m.tpCavIDToRevCavID[thirdPartyCaveatID])], nil
+}
diff --git a/services/identity/revocation/revocation_test.go b/services/identity/revocation/revocation_test.go
index 09034c9..147882c 100644
--- a/services/identity/revocation/revocation_test.go
+++ b/services/identity/revocation/revocation_test.go
@@ -3,7 +3,6 @@
import (
"bytes"
"testing"
- "time"
"veyron.io/veyron/veyron2"
"veyron.io/veyron/veyron2/naming"
@@ -16,38 +15,8 @@
"veyron.io/veyron/veyron/services/security/discharger"
)
-type mockDatabase struct {
- tpCavIDToRevCavID map[string][]byte
- revCavIDToTimestamp map[string]*time.Time
-}
-
-func (m *mockDatabase) InsertCaveat(thirdPartyCaveatID string, revocationCaveatID []byte) error {
- m.tpCavIDToRevCavID[thirdPartyCaveatID] = revocationCaveatID
- return nil
-}
-
-func (m *mockDatabase) Revoke(thirdPartyCaveatID string) error {
- timestamp := time.Now()
- m.revCavIDToTimestamp[string(m.tpCavIDToRevCavID[thirdPartyCaveatID])] = ×tamp
- return nil
-}
-
-func (m *mockDatabase) IsRevoked(revocationCaveatID []byte) (bool, error) {
- _, exists := m.revCavIDToTimestamp[string(revocationCaveatID)]
- return exists, nil
-}
-
-func (m *mockDatabase) RevocationTime(thirdPartyCaveatID string) (*time.Time, error) {
- return m.revCavIDToTimestamp[string(m.tpCavIDToRevCavID[thirdPartyCaveatID])], nil
-}
-
-func newRevocationManager(t *testing.T) RevocationManager {
- revocationDB = &mockDatabase{make(map[string][]byte), make(map[string]*time.Time)}
- return &revocationManager{}
-}
-
func revokerSetup(t *testing.T, r veyron2.Runtime) (dischargerKey security.PublicKey, dischargerEndpoint string, revoker RevocationManager, closeFunc func(), runtime veyron2.Runtime) {
- revokerService := newRevocationManager(t)
+ revokerService := NewMockRevocationManager()
dischargerServer, err := r.NewServer()
if err != nil {
t.Fatalf("r.NewServer: %s", err)
diff --git a/services/identity/server/identityd.go b/services/identity/server/identityd.go
index 6e767f4..399364e 100644
--- a/services/identity/server/identityd.go
+++ b/services/identity/server/identityd.go
@@ -25,6 +25,7 @@
"veyron.io/veyron/veyron/security/audit"
"veyron.io/veyron/veyron/services/identity/auditor"
"veyron.io/veyron/veyron/services/identity/blesser"
+ "veyron.io/veyron/veyron/services/identity/caveats"
"veyron.io/veyron/veyron/services/identity/handlers"
"veyron.io/veyron/veyron/services/identity/oauth"
"veyron.io/veyron/veyron/services/identity/revocation"
@@ -53,6 +54,7 @@
blessingLogReader auditor.BlessingLogReader
revocationManager revocation.RevocationManager
oauthBlesserParams blesser.GoogleParams
+ caveatSelector caveats.CaveatSelector
}
// NewIdentityServer returns a IdentityServer that:
@@ -60,13 +62,14 @@
// - auditor and blessingLogReader to audit the root principal and read audit logs
// - revocationManager to store revocation data and grant discharges
// - oauthBlesserParams to configure the identity.OAuthBlesser service
-func NewIdentityServer(oauthProvider oauth.OAuthProvider, auditor audit.Auditor, blessingLogReader auditor.BlessingLogReader, revocationManager revocation.RevocationManager, oauthBlesserParams blesser.GoogleParams) *identityd {
+func NewIdentityServer(oauthProvider oauth.OAuthProvider, auditor audit.Auditor, blessingLogReader auditor.BlessingLogReader, revocationManager revocation.RevocationManager, oauthBlesserParams blesser.GoogleParams, caveatSelector caveats.CaveatSelector) *identityd {
return &identityd{
oauthProvider,
auditor,
blessingLogReader,
revocationManager,
oauthBlesserParams,
+ caveatSelector,
}
}
@@ -103,6 +106,7 @@
DischargerLocation: naming.JoinAddressName(published[0], dischargerService),
MacaroonBlessingService: naming.JoinAddressName(published[0], macaroonService),
OAuthProvider: s.oauthProvider,
+ CaveatSelector: s.caveatSelector,
})
if err != nil {
vlog.Fatalf("Failed to create HTTP handler for oauth authentication: %v", err)
diff --git a/services/identity/test.sh b/services/identity/test.sh
new file mode 100755
index 0000000..6a20639
--- /dev/null
+++ b/services/identity/test.sh
@@ -0,0 +1,81 @@
+#!/bin/bash
+
+# Test that tests the routes of the identityd server.
+
+source "$(go list -f {{.Dir}} veyron.io/veyron/shell/lib)/shell_test.sh"
+
+readonly WORKDIR="${shell_test_WORK_DIR}"
+
+build() {
+ IDENTITYD_BIN="$(shell_test::build_go_binary 'veyron.io/veyron/veyron/services/identity/identityd_test')"
+ PRINCIPAL_BIN="$(shell_test::build_go_binary 'veyron.io/veyron/veyron/tools/principal')"
+}
+
+# These certificatese were created with "generate_cert.go --host=localhost --duration=87600h --ecdsa-curve=P256"
+CERT="-----BEGIN CERTIFICATE-----
+MIIBbTCCARSgAwIBAgIRANKYmC0v3pK+VohyJOdD1hgwCgYIKoZIzj0EAwIwEjEQ
+MA4GA1UEChMHQWNtZSBDbzAeFw0xNDExMjEyMjEwNTJaFw0yNDExMTgyMjEwNTJa
+MBIxEDAOBgNVBAoTB0FjbWUgQ28wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASv
+heWcWcZT7d5Sm/uoWhBUJJPBSREN4qGzBV7yFYUFvHJ9mNaEcopo/6BopJRbvUmj
+CQMVDZVMm5Er/f8HgCngo0swSTAOBgNVHQ8BAf8EBAMCAKAwEwYDVR0lBAwwCgYI
+KwYBBQUHAwEwDAYDVR0TAQH/BAIwADAUBgNVHREEDTALgglsb2NhbGhvc3QwCgYI
+KoZIzj0EAwIDRwAwRAIgAkwh+mi5YlIxYzxzT7bQj/ZYU5pufxHt+F+a75gbm7AC
+IAI9+axCPawySY+UYvjO14hklsyy3LnSf1mNHyeGydMM
+-----END CERTIFICATE-----"
+
+KEY="-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIHxiR6vjOn1jF1KS0V//pXrulxss9PwUgV/7/QVeV2zCoAoGCCqGSM49
+AwEHoUQDQgAEr4XlnFnGU+3eUpv7qFoQVCSTwUkRDeKhswVe8hWFBbxyfZjWhHKK
+aP+gaKSUW71JowkDFQ2VTJuRK/3/B4Ap4A==
+-----END EC PRIVATE KEY-----"
+
+# runprincipal starts the principal tool, extracts the url and curls it, to avoid the
+# dependence the principal tool has on a browser.
+runprincipal() {
+ local PFILE="${WORKDIR}/principalfile"
+ # Start the tool in the background.
+ "${PRINCIPAL_BIN}" seekblessings --browser=false --from=https://localhost:8125/google -v=3 2> "${PFILE}" &
+ sleep 2
+ # Search for the url and run it.
+ cat "${PFILE}" | grep https |
+ while read url; do
+ RESULT=$(curl -L --insecure -c ${WORKDIR}/cookiejar $url);
+ # Clear out the file
+ echo $RESULT;
+ break;
+ done;
+ rm "${PFILE}";
+}
+
+main() {
+ cd "${WORKDIR}"
+ build
+
+ # Setup the certificate files.
+ echo "${CERT}" > "${WORKDIR}/cert.pem"
+ echo "${KEY}" > "${WORKDIR}/key.pem"
+
+ shell_test::setup_server_test || shell_test::fail "line ${LINENO} failed to setup server test"
+ unset VEYRON_CREDENTIALS
+
+ # Start the identityd server in test identity server.
+ shell_test::start_server "${IDENTITYD_BIN}" --host=localhost --tlsconfig="${WORKDIR}/cert.pem,${WORKDIR}/key.pem" -veyron.tcp.address=127.0.0.1:0
+ echo Identityd Log File: $START_SERVER_LOG_FILE
+ export VEYRON_CREDENTIALS="$(shell::tmp_dir)"
+
+ # Test an initial seekblessings call, with a specified VEYRON_CREDENTIALS.
+ WANT="Received blessings"
+ GOT=$(runprincipal)
+ if [[ ! "${GOT}" =~ "${WANT}" ]]; then
+ shell_test::fail "line ${LINENO} failed first seekblessings call"
+ fi
+ # Test that a subsequent call succeed with the same credentials. This means that the blessings and principal from the first call works correctly.
+ GOT=$(runprincipal)
+ if [[ ! "${GOT}" =~ "${WANT}" ]]; then
+ shell_test::fail "line ${LINENO} failed second seekblessings call"
+ fi
+
+ shell_test::pass
+}
+
+main "$@"
\ No newline at end of file
diff --git a/tools/principal/bless.go b/tools/principal/bless.go
index d009b69..be171b2 100644
--- a/tools/principal/bless.go
+++ b/tools/principal/bless.go
@@ -16,7 +16,7 @@
"veyron.io/veyron/veyron2/vlog"
)
-func getMacaroonForBlessRPC(blessServerURL string, blessedChan <-chan string) (<-chan string, error) {
+func getMacaroonForBlessRPC(blessServerURL string, blessedChan <-chan string, browser bool) (<-chan string, error) {
// Setup a HTTP server to recieve a blessing macaroon from the identity server.
// Steps:
// 1. Generate a state token to be included in the HTTP request
@@ -85,7 +85,7 @@
// Use exec.Command().Start instead of exec.Command().Run since there is no
// need to wait for the command to return (and indeed on some window managers,
// the command will not exit until the browser is closed).
- if len(openCommand) != 0 {
+ if len(openCommand) != 0 && browser {
exec.Command(openCommand, url).Start()
}
return result, nil
diff --git a/tools/principal/main.go b/tools/principal/main.go
index 432ef60..3f9ed67 100644
--- a/tools/principal/main.go
+++ b/tools/principal/main.go
@@ -42,6 +42,7 @@
flagSeekBlessingsFrom string
flagSeekBlessingsSetDefault bool
flagSeekBlessingsForPeer string
+ flagSeekBlessingsBrowser bool
// Flags common to many commands
flagAddToRoots bool
@@ -593,7 +594,7 @@
blessedChan := make(chan string)
defer close(blessedChan)
- macaroonChan, err := getMacaroonForBlessRPC(flagSeekBlessingsFrom, blessedChan)
+ macaroonChan, err := getMacaroonForBlessRPC(flagSeekBlessingsFrom, blessedChan, flagSeekBlessingsBrowser)
if err != nil {
return fmt.Errorf("failed to get macaroon from Veyron blesser: %v", err)
}
@@ -729,6 +730,7 @@
cmdSeekBlessings.Flags.StringVar(&flagSeekBlessingsFrom, "from", "https://auth.dev.v.io:8125/google", "URL to use to begin the seek blessings process")
cmdSeekBlessings.Flags.BoolVar(&flagSeekBlessingsSetDefault, "set_default", true, "If true, the blessings obtained will be set as the default blessing in the store")
cmdSeekBlessings.Flags.StringVar(&flagSeekBlessingsForPeer, "for_peer", string(security.AllPrincipals), "If non-empty, the blessings obtained will be marked for peers matching this pattern in the store")
+ cmdSeekBlessings.Flags.BoolVar(&flagSeekBlessingsBrowser, "browser", true, "If false, the seekblessings command will not open the browser and only print the url to visit.")
cmdSeekBlessings.Flags.BoolVar(&flagAddToRoots, "add_to_roots", true, "If true, the root certificate of the blessing will be added to the principal's set of recognized root certificates")
cmdStoreSet.Flags.BoolVar(&flagAddToRoots, "add_to_roots", true, "If true, the root certificate of the blessing will be added to the principal's set of recognized root certificates")