blob: a90ccb6892e732b0527dd794153a2b0d2e5c45b9 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package security
import (
"time"
"v.io/v23"
"v.io/v23/context"
"v.io/v23/security"
"v.io/v23/vom"
)
// DischargeRefreshFraction determines how early before their expiration
// time we refresh discharges. A value of 0.5 means we refresh when it
// is only half way to is expiration time.
const DischargeRefreshFraction = 0.5
// If this is attached to the context, we will not fetch discharges.
// We use this to prevent ourselves from fetching discharges in the
// process of fetching discharges, thus creating an infinite loop.
type skipDischargesKey struct{}
type updateResult struct {
refreshTime time.Time
discharge security.Discharge
done bool
}
type work struct {
caveat security.Caveat
impetus security.DischargeImpetus
}
// PrepareDischarges retrieves the caveat discharges required for using blessings
// at server. The discharges are either found in the dischargeCache, in the call
// options, or requested from the discharge issuer indicated on the caveat.
// Note that requesting a discharge is an rpc call, so one copy of this
// function must be able to successfully terminate while another is blocked.
// PrepareDischarges also returns a refreshTime, which is the time at which
// PrepareDischarges should be called again (or zero if none of the discharges
// expire).
func PrepareDischarges(
ctx *context.T,
blessings security.Blessings,
serverBlessings []string,
method string,
args []interface{}) (map[string]security.Discharge, time.Time) {
tpCavs := blessings.ThirdPartyCaveats()
if len(tpCavs) == 0 {
return nil, time.Time{}
}
// We only want to send the impetus information we really need for each
// discharge.
todo := make(map[string]work, len(tpCavs))
for _, cav := range tpCavs {
if tp := cav.ThirdPartyDetails(); tp != nil {
todo[tp.ID()] = work{cav, filteredImpetus(tp.Requirements(), serverBlessings, method, args)}
}
}
// Since there may be dependencies in the caveats, we keep retrying
// until either all discharges can be fetched or no new discharges
// are fetched.
var minRefreshTime time.Time
ch := make(chan *updateResult, len(tpCavs))
ret := make(map[string]security.Discharge, len(tpCavs))
for {
want := len(todo)
now := time.Now()
for _, w := range todo {
updateDischarge(ctx, now, w.impetus, w.caveat, ch)
}
got := 0
for i := 0; i < want; i++ {
res := <-ch
id := res.discharge.ID()
ret[id] = res.discharge
if res.done {
minRefreshTime = minTime(minRefreshTime, res.refreshTime)
delete(todo, id)
got++
}
}
if got == want {
return ret, minRefreshTime
}
if got == 0 {
return ret, minTime(minRefreshTime, time.Now().Add(time.Minute))
}
}
}
func updateDischarge(
ctx *context.T,
now time.Time,
impetus security.DischargeImpetus,
caveat security.Caveat,
out chan<- *updateResult) {
bstore := v23.GetPrincipal(ctx).BlessingStore()
dis, ct := bstore.Discharge(caveat, impetus)
if skip, _ := ctx.Value(skipDischargesKey{}).(bool); skip {
// We can't fetch discharges while making a call to fetch a discharge.
// Just go with what we have in the cache.
out <- &updateResult{done: true, discharge: dis}
return
}
if rt := refreshTime(dis, ct); dis.ID() != "" && (now.Before(rt) || rt.IsZero()) {
// The cached value is still fresh, just return it.
out <- &updateResult{done: true, discharge: dis, refreshTime: rt}
return
}
// discharge in blessing store either doesn't exist or may be stale,
// refresh via RPC.
go func() {
tp := caveat.ThirdPartyDetails()
ctx = context.WithValue(ctx, skipDischargesKey{}, true)
var newDis security.Discharge
args, res := []interface{}{caveat, impetus}, []interface{}{&newDis}
ctx.VI(3).Infof("Fetching discharge for %v", tp)
if err := v23.GetClient(ctx).Call(ctx, tp.Location(), "Discharge", args, res); err != nil {
ctx.VI(3).Infof("Discharge fetch for %v failed: %v", tp, err)
out <- &updateResult{discharge: dis}
return
}
bstore.CacheDischarge(newDis, caveat, impetus)
out <- &updateResult{done: true, discharge: newDis, refreshTime: refreshTime(newDis, time.Now())}
}()
}
// filteredImpetus returns a copy of 'before' after removing any values that are not required as per 'r'.
func filteredImpetus(r security.ThirdPartyRequirements, serverBlessings []string, method string, args []interface{}) security.DischargeImpetus {
if !r.ReportServer {
serverBlessings = nil
}
if !r.ReportMethod {
method = ""
}
if !r.ReportArguments {
args = nil
}
return mkDischargeImpetus(serverBlessings, method, args)
}
func refreshTime(dis security.Discharge, cacheTime time.Time) time.Time {
expiry := dis.Expiry()
if expiry.IsZero() {
return time.Time{}
}
if cacheTime.IsZero() {
// If we don't know the cache time, just try to refresh within a minute of
// the expiry time.
return expiry.Add(-time.Minute)
}
lifetime := expiry.Sub(cacheTime)
return expiry.Add(-time.Duration(float64(lifetime) * DischargeRefreshFraction))
}
func minTime(a, b time.Time) time.Time {
if a.IsZero() || (!b.IsZero() && b.Before(a)) {
return b
}
return a
}
func mkDischargeImpetus(serverBlessings []string, method string, args []interface{}) security.DischargeImpetus {
var impetus security.DischargeImpetus
if len(serverBlessings) > 0 {
impetus.Server = make([]security.BlessingPattern, len(serverBlessings))
for i, b := range serverBlessings {
impetus.Server[i] = security.BlessingPattern(b)
}
}
impetus.Method = method
if len(args) > 0 {
impetus.Arguments = make([]*vom.RawBytes, len(args))
for i, a := range args {
vArg, err := vom.RawBytesFromValue(a)
if err != nil {
continue
}
impetus.Arguments[i] = vArg
}
}
return impetus
}