| // 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 |
| } |