1// Copyright 2015 Keybase, Inc. All rights reserved. Use of
2// this source code is governed by the included BSD license.
3
4package libkb
5
6import (
7	"fmt"
8	"sync"
9	"time"
10
11	"runtime/debug"
12
13	keybase1 "github.com/keybase/client/go/protocol/keybase1"
14	jsonw "github.com/keybase/go-jsonw"
15	"stathat.com/c/ramcache"
16)
17
18type ResolveResult struct {
19	uid                keybase1.UID
20	teamID             keybase1.TeamID
21	body               *jsonw.Wrapper
22	err                error
23	queriedKbUsername  string
24	queriedByUID       bool
25	resolvedKbUsername string
26	queriedByTeamID    bool
27	resolvedTeamName   keybase1.TeamName
28	cachedAt           time.Time
29	mutable            bool
30	deleted            bool
31	isCompound         bool
32	isServerTrust      bool
33}
34
35func (res ResolveResult) HasPrimaryKey() bool {
36	return res.uid.Exists() || res.teamID.Exists()
37}
38
39func (res ResolveResult) String() string {
40	return fmt.Sprintf("{uid:%s teamID:%s err:%s mutable:%v}", res.uid, res.teamID, ErrToOk(res.err), res.mutable)
41}
42
43const (
44	resolveCacheTTL           = 12 * time.Hour
45	ResolveCacheMaxAge        = 12 * time.Hour
46	ResolveCacheMaxAgeMutable = 20 * time.Minute
47	resolveCacheMaxAgeErrored = 5 * time.Second
48)
49
50func (res *ResolveResult) GetUID() keybase1.UID {
51	return res.uid
52}
53
54func (res *ResolveResult) SetUIDForTesting(u keybase1.UID) {
55	res.uid = u
56}
57
58func (res *ResolveResult) User() keybase1.User {
59	return keybase1.User{
60		Uid:      res.GetUID(),
61		Username: res.GetNormalizedUsername().String(),
62	}
63}
64
65func (res *ResolveResult) UserOrTeam() keybase1.UserOrTeamLite {
66	var u keybase1.UserOrTeamLite
67	if res.GetUID().Exists() {
68		u.Id, u.Name = res.GetUID().AsUserOrTeam(), res.GetNormalizedUsername().String()
69	} else if res.GetTeamID().Exists() {
70		u.Id, u.Name = res.GetTeamID().AsUserOrTeam(), res.GetTeamName().String()
71	}
72	return u
73}
74
75func (res *ResolveResult) GetUsername() string {
76	return res.resolvedKbUsername
77}
78func (res *ResolveResult) GetNormalizedUsername() NormalizedUsername {
79	return NewNormalizedUsername(res.GetUsername())
80}
81func (res *ResolveResult) GetNormalizedQueriedUsername() NormalizedUsername {
82	return NewNormalizedUsername(res.queriedKbUsername)
83}
84
85func (res *ResolveResult) WasTeamIDAssertion() bool {
86	return res.queriedByTeamID
87}
88
89func (res *ResolveResult) GetTeamID() keybase1.TeamID {
90	return res.teamID
91}
92func (res *ResolveResult) GetTeamName() keybase1.TeamName {
93	return res.resolvedTeamName
94}
95
96func (res *ResolveResult) WasKBAssertion() bool {
97	return (res.queriedKbUsername != "" && !res.isCompound) || res.queriedByUID
98}
99
100func (res *ResolveResult) GetError() error {
101	return res.err
102}
103
104func (res *ResolveResult) GetBody() *jsonw.Wrapper {
105	return res.body
106}
107
108func (res *ResolveResult) GetDeleted() bool {
109	return res.deleted
110}
111
112func (res ResolveResult) FailOnDeleted() ResolveResult {
113	if res.deleted {
114		label := res.uid.String()
115		if res.resolvedKbUsername != "" {
116			label = res.resolvedKbUsername
117		}
118		res.err = UserDeletedError{Msg: fmt.Sprintf("user %q deleted", label)}
119	}
120	return res
121}
122
123func (res ResolveResult) IsServerTrust() bool {
124	return res.isServerTrust
125}
126
127func (r *ResolverImpl) ResolveWithBody(m MetaContext, input string) ResolveResult {
128	return r.resolve(m, input, true)
129}
130
131func (r *ResolverImpl) Resolve(m MetaContext, input string) ResolveResult {
132	return r.resolve(m, input, false)
133}
134
135func (r *ResolverImpl) resolve(m MetaContext, input string, withBody bool) (res ResolveResult) {
136	defer m.Trace(fmt.Sprintf("Resolving username %q", input), &res.err)()
137
138	var au AssertionURL
139	if au, res.err = ParseAssertionURL(m.G().MakeAssertionContext(m), input, false); res.err != nil {
140		return res
141	}
142	res = r.resolveURL(m, au, input, withBody, false)
143	return res
144}
145
146func (r *ResolverImpl) ResolveFullExpression(m MetaContext, input string) (res ResolveResult) {
147	return r.resolveFullExpression(m, input, false, false)
148}
149
150func (r *ResolverImpl) ResolveFullExpressionNeedUsername(m MetaContext, input string) (res ResolveResult) {
151	return r.resolveFullExpression(m, input, false, true)
152}
153
154func (r *ResolverImpl) ResolveFullExpressionWithBody(m MetaContext, input string) (res ResolveResult) {
155	return r.resolveFullExpression(m, input, true, false)
156}
157
158func (r *ResolverImpl) ResolveUser(m MetaContext, assertion string) (u keybase1.User, res ResolveResult, err error) {
159	res = r.ResolveFullExpressionNeedUsername(m, assertion)
160	err = res.GetError()
161	if err != nil {
162		return u, res, err
163	}
164	u = res.User()
165	if !u.Uid.Exists() {
166		return u, res, fmt.Errorf("no resolution for: %v", assertion)
167	}
168	return u, res, nil
169}
170
171func (r *ResolverImpl) resolveFullExpression(m MetaContext, input string, withBody bool, needUsername bool) (res ResolveResult) {
172	defer m.VTrace(VLog1, fmt.Sprintf("Resolver#resolveFullExpression(%q)", input), &res.err)()
173
174	var expr AssertionExpression
175	expr, res.err = AssertionParseAndOnly(m.G().MakeAssertionContext(m), input)
176	if res.err != nil {
177		return res
178	}
179	u := FindBestIdentifyComponentURL(expr)
180	if u == nil {
181		res.err = ResolutionError{Input: input, Msg: "Cannot find a resolvable factor"}
182		return res
183	}
184	ret := r.resolveURL(m, u, input, withBody, needUsername)
185	ret.isCompound = len(expr.CollectUrls(nil)) > 1
186	return ret
187}
188
189func (res *ResolveResult) addKeybaseNameIfKnown(au AssertionURL) {
190	if au.IsKeybase() && len(res.resolvedKbUsername) == 0 {
191		res.resolvedKbUsername = au.GetValue()
192	}
193}
194
195func (r *ResolverImpl) getFromDiskCache(m MetaContext, key string, au AssertionURL) (ret *ResolveResult) {
196	defer m.VTrace(VLog1, fmt.Sprintf("Resolver#getFromDiskCache(%q)", key), nil)()
197	var uid keybase1.UID
198	found, err := m.G().LocalDb.GetInto(&uid, resolveDbKey(key))
199	r.Stats.IncDiskGets()
200	if err != nil {
201		m.Warning("Problem fetching resolve result from local DB: %s", err)
202		return nil
203	}
204	if !found {
205		r.Stats.IncDiskGetMisses()
206		return nil
207	}
208	if uid.IsNil() {
209		m.Warning("nil UID found in disk cache")
210		return nil
211	}
212	r.Stats.IncDiskGetHits()
213	return &ResolveResult{uid: uid}
214}
215
216func isMutable(au AssertionURL) bool {
217	isStatic := au.IsUID() ||
218		au.IsKeybase() ||
219		(au.IsTeamID() && !au.ToTeamID().IsSubTeam()) ||
220		(au.IsTeamName() && au.ToTeamName().IsRootTeam())
221	return !isStatic
222}
223
224func (r *ResolverImpl) getFromUPAKLoader(m MetaContext, uid keybase1.UID) (ret *ResolveResult) {
225	nun, err := m.G().GetUPAKLoader().LookupUsername(m.Ctx(), uid)
226	if err != nil {
227		return nil
228	}
229	return &ResolveResult{uid: uid, queriedByUID: true, resolvedKbUsername: nun.String(), mutable: false}
230}
231
232func (r *ResolverImpl) resolveURL(m MetaContext, au AssertionURL, input string, withBody bool, needUsername bool) (res ResolveResult) {
233	ck := au.CacheKey()
234
235	lock := r.locktab.AcquireOnName(m.Ctx(), m.G(), ck)
236	defer lock.Release(m.Ctx())
237
238	// Debug succinctly what happened in the resolution
239	var trace string
240	defer func() {
241		m.Debug("| Resolver#resolveURL(%s) -> %s [trace:%s]", ck, res, trace)
242	}()
243
244	// A standard keybase UID, so it's already resolved... unless we explicitly
245	// need it!
246	if !needUsername {
247		if tmp := au.ToUID(); tmp.Exists() {
248			trace += "u"
249			return ResolveResult{uid: tmp}
250		}
251	}
252
253	if p := r.getFromMemCache(m, ck, au); p != nil && (!needUsername || len(p.resolvedKbUsername) > 0 || !p.resolvedTeamName.IsNil()) {
254		trace += "m"
255		ret := *p
256		ret.decorate(au)
257		return ret
258	}
259
260	if p := r.getFromDiskCache(m, ck, au); p != nil && (!needUsername || len(p.resolvedKbUsername) > 0 || !p.resolvedTeamName.IsNil()) {
261		p.mutable = isMutable(au)
262		r.putToMemCache(m, ck, *p)
263		trace += "d"
264		ret := *p
265		ret.decorate(au)
266		return ret
267	}
268
269	// We can check the UPAK loader for the username if we're just mapping a UID to a username.
270	if tmp := au.ToUID(); !withBody && tmp.Exists() {
271		if p := r.getFromUPAKLoader(m, tmp); p != nil {
272			trace += "l"
273			r.putToMemCache(m, ck, *p)
274			return *p
275		}
276	}
277
278	trace += "s"
279	res = r.resolveURLViaServerLookup(m, au, input, withBody)
280
281	// Cache for a shorter period of time if it's not a Keybase identity
282	res.mutable = isMutable(au)
283	r.putToMemCache(m, ck, res)
284
285	// We only put to disk cache if it's a Keybase-type assertion. In
286	// particular, UIDs are **not** stored to disk.
287	if au.IsKeybase() {
288		trace += "p"
289		r.putToDiskCache(m, ck, res)
290	}
291
292	return res
293}
294
295func (res *ResolveResult) decorate(au AssertionURL) {
296	if au.IsKeybase() {
297		res.queriedKbUsername = au.GetValue()
298	} else if au.IsUID() {
299		res.queriedByUID = true
300	}
301}
302
303func (r *ResolverImpl) resolveURLViaServerLookup(m MetaContext, au AssertionURL, input string, withBody bool) (res ResolveResult) {
304	defer m.VTrace(VLog1, fmt.Sprintf("Resolver#resolveURLViaServerLookup(input = %q)", input), &res.err)()
305
306	if au.IsTeamID() || au.IsTeamName() {
307		return r.resolveTeamViaServerLookup(m, au)
308	}
309
310	if au.IsServerTrust() {
311		return r.resolveServerTrustAssertion(m, au, input)
312	}
313
314	var key, val string
315	var ares *APIRes
316	var l int
317
318	res.decorate(au)
319
320	if key, val, res.err = au.ToLookup(); res.err != nil {
321		return
322	}
323
324	ha := HTTPArgsFromKeyValuePair(key, S{val})
325	ha.Add("multi", I{1})
326	ha.Add("load_deleted_v2", B{true})
327	fields := "basics"
328	if withBody {
329		fields += ",public_keys,pictures"
330	}
331	ha.Add("fields", S{fields})
332	ares, res.err = m.G().API.Get(m, APIArg{
333		Endpoint:        "user/lookup",
334		SessionType:     APISessionTypeNONE,
335		Args:            ha,
336		AppStatusCodes:  []int{SCOk, SCNotFound, SCDeleted},
337		RetryCount:      3,
338		InitialTimeout:  4 * time.Second,
339		RetryMultiplier: 1.5,
340	})
341
342	if res.err != nil {
343		m.Debug("API user/lookup %q error: %s", input, res.err)
344		return
345	}
346	switch ares.AppStatus.Code {
347	case SCNotFound:
348		m.Debug("API user/lookup %q not found", input)
349		res.err = NotFoundError{}
350		return
351	default:
352		// Nothing to do for other codes.
353	}
354
355	var them *jsonw.Wrapper
356	if them, res.err = ares.Body.AtKey("them").ToArray(); res.err != nil {
357		return res
358	}
359
360	if l, res.err = them.Len(); res.err != nil {
361		return res
362	}
363
364	if l == 0 {
365		res.err = ResolutionError{Input: input, Msg: "No resolution found", Kind: ResolutionErrorNotFound}
366		return res
367	}
368	if l > 1 {
369		res.err = ResolutionError{Input: input, Msg: "Identify is ambiguous", Kind: ResolutionErrorAmbiguous}
370		return res
371	}
372	res.body = them.AtIndex(0)
373	res.uid, res.err = GetUID(res.body.AtKey("id"))
374	if res.err != nil {
375		return res
376	}
377	res.resolvedKbUsername, res.err = res.body.AtPath("basics.username").GetString()
378	if res.err != nil {
379		return res
380	}
381	var status int
382	status, res.err = res.body.AtPath("basics.status").GetInt()
383	if res.err != nil {
384		return res
385	}
386	if status == SCDeleted {
387		res.deleted = true
388	}
389
390	return res
391}
392
393type teamLookup struct {
394	ID     keybase1.TeamID   `json:"id"`
395	Name   keybase1.TeamName `json:"name"`
396	Status AppStatus         `json:"status"`
397}
398
399func (t *teamLookup) GetAppStatus() *AppStatus {
400	return &t.Status
401}
402
403func (r *ResolverImpl) resolveTeamViaServerLookup(m MetaContext, au AssertionURL) (res ResolveResult) {
404	m.Debug("resolveTeamViaServerLookup")
405
406	res.queriedByTeamID = au.IsTeamID()
407	key, val, err := au.ToLookup()
408	if err != nil {
409		res.err = err
410		return res
411	}
412
413	arg := NewAPIArg("team/get")
414	arg.SessionType = APISessionTypeREQUIRED
415	arg.Args = make(HTTPArgs)
416	arg.Args[key] = S{Val: val}
417	arg.Args["lookup_only"] = B{Val: true}
418	if res.queriedByTeamID && au.ToTeamID().IsPublic() {
419		arg.Args["public"] = B{Val: true}
420	}
421
422	var lookup teamLookup
423	if err := m.G().API.GetDecode(m, arg, &lookup); err != nil {
424		res.err = err
425		return res
426	}
427
428	res.resolvedTeamName = lookup.Name
429	res.teamID = lookup.ID
430
431	return res
432}
433
434type serverTrustUserLookup struct {
435	AppStatusEmbed
436	User *keybase1.PhoneLookupResult `json:"user"`
437}
438
439func (r *ResolverImpl) resolveServerTrustAssertion(m MetaContext, au AssertionURL, input string) (res ResolveResult) {
440	defer m.Trace(fmt.Sprintf("Resolver#resolveServerTrustAssertion(%q, %q)", au.String(), input), &res.err)()
441
442	key, val, err := au.ToLookup()
443	if err != nil {
444		res.err = err
445		return res
446	}
447
448	var arg APIArg
449	switch key {
450	case "phone":
451		arg = NewAPIArg("user/phone_numbers_search")
452		arg.Args = map[string]HTTPValue{"phone_number": S{Val: val}}
453	case "email":
454		arg = NewAPIArg("email/search")
455		arg.Args = map[string]HTTPValue{"email": S{Val: val}}
456	default:
457		res.err = ResolutionError{Input: input, Msg: fmt.Sprintf("Unexpected assertion: %q for server trust lookup", key), Kind: ResolutionErrorInvalidInput}
458		return res
459	}
460
461	arg.SessionType = APISessionTypeREQUIRED
462	arg.AppStatusCodes = []int{SCOk}
463
464	var lookup serverTrustUserLookup
465	if err := m.G().API.GetDecode(m, arg, &lookup); err != nil {
466		if appErr, ok := err.(AppStatusError); ok {
467			switch appErr.Code {
468			case SCInputError:
469				res.err = ResolutionError{Input: input, Msg: err.Error(), Kind: ResolutionErrorInvalidInput}
470				return res
471			case SCRateLimit:
472				res.err = ResolutionError{Input: input, Msg: err.Error(), Kind: ResolutionErrorRateLimited}
473				return res
474			}
475		}
476		// When the call fails because of timeout or other reason, stop
477		// the process as well. Same reason as other errors - we don't
478		// want to create dead SBS team when there was a resolvable user
479		// but we weren't able to resolve.
480		res.err = ResolutionError{Input: input, Msg: err.Error(), Kind: ResolutionErrorRequestFailed}
481		return res
482	}
483
484	if lookup.User == nil {
485		res.err = ResolutionError{Input: input, Msg: "No resolution found", Kind: ResolutionErrorNotFound}
486		return res
487	}
488
489	user := *lookup.User
490	res.resolvedKbUsername = user.Username
491	res.uid = user.Uid
492	res.isServerTrust = true
493	// Mutable resolutions are not cached to disk. We can't be aggressive when
494	// caching server-trust resolutions, because when client pulls out one from
495	// cache, they have no way to verify it's still valid. From the server-side
496	// we have no way to invalidate that cache.
497	res.mutable = true
498
499	return res
500}
501
502type ResolveCacheStats struct {
503	sync.Mutex
504	misses          int
505	timeouts        int
506	mutableTimeouts int
507	errorTimeouts   int
508	hits            int
509	diskGets        int
510	diskGetHits     int
511	diskGetMisses   int
512	diskPuts        int
513}
514
515type ResolverImpl struct {
516	cache   *ramcache.Ramcache
517	Stats   *ResolveCacheStats
518	locktab *LockTable
519}
520
521func (s *ResolveCacheStats) Eq(m, t, mt, et, h int) bool {
522	s.Lock()
523	defer s.Unlock()
524	return (s.misses == m) && (s.timeouts == t) && (s.mutableTimeouts == mt) && (s.errorTimeouts == et) && (s.hits == h)
525}
526
527func (s *ResolveCacheStats) EqWithDiskHits(m, t, mt, et, h, dh int) bool {
528	s.Lock()
529	defer s.Unlock()
530	return (s.misses == m) && (s.timeouts == t) && (s.mutableTimeouts == mt) && (s.errorTimeouts == et) && (s.hits == h) && (s.diskGetHits == dh)
531}
532
533func (s *ResolveCacheStats) IncMisses() {
534	s.Lock()
535	s.misses++
536	s.Unlock()
537}
538
539func (s *ResolveCacheStats) IncTimeouts() {
540	s.Lock()
541	s.timeouts++
542	s.Unlock()
543}
544
545func (s *ResolveCacheStats) IncMutableTimeouts() {
546	s.Lock()
547	s.mutableTimeouts++
548	s.Unlock()
549}
550
551func (s *ResolveCacheStats) IncErrorTimeouts() {
552	s.Lock()
553	s.errorTimeouts++
554	s.Unlock()
555}
556
557func (s *ResolveCacheStats) IncHits() {
558	s.Lock()
559	s.hits++
560	s.Unlock()
561}
562
563func (s *ResolveCacheStats) IncDiskGets() {
564	s.Lock()
565	s.diskGets++
566	s.Unlock()
567}
568
569func (s *ResolveCacheStats) IncDiskGetHits() {
570	s.Lock()
571	s.diskGetHits++
572	s.Unlock()
573}
574
575func (s *ResolveCacheStats) IncDiskGetMisses() {
576	s.Lock()
577	s.diskGetMisses++
578	s.Unlock()
579}
580
581func (s *ResolveCacheStats) IncDiskPuts() {
582	s.Lock()
583	s.diskPuts++
584	s.Unlock()
585}
586
587func NewResolverImpl() *ResolverImpl {
588	return &ResolverImpl{
589		cache:   nil,
590		locktab: NewLockTable(),
591		Stats:   &ResolveCacheStats{},
592	}
593}
594
595func (r *ResolverImpl) EnableCaching(m MetaContext) {
596	cache := ramcache.New()
597	cache.MaxAge = ResolveCacheMaxAge
598	cache.TTL = resolveCacheTTL
599	r.cache = cache
600}
601
602func (r *ResolverImpl) Shutdown(m MetaContext) {
603	if r.cache == nil {
604		return
605	}
606	r.cache.Shutdown()
607}
608
609func (r *ResolverImpl) getFromMemCache(m MetaContext, key string, au AssertionURL) (ret *ResolveResult) {
610	defer m.VTrace(VLog1, fmt.Sprintf("Resolver#getFromMemCache(%q)", key), nil)()
611	if r.cache == nil {
612		return nil
613	}
614	res, _ := r.cache.Get(key)
615	if res == nil {
616		r.Stats.IncMisses()
617		return nil
618	}
619	rres, ok := res.(*ResolveResult)
620	if !ok {
621		r.Stats.IncMisses()
622		return nil
623	}
624	// Should never happen, but don't corrupt application state if it does
625	if !rres.HasPrimaryKey() {
626		m.Info("Resolver#getFromMemCache: nil UID/teamID in cache")
627		return nil
628	}
629	now := m.G().Clock().Now()
630	if now.Sub(rres.cachedAt) > ResolveCacheMaxAge {
631		r.Stats.IncTimeouts()
632		return nil
633	}
634	if rres.mutable && now.Sub(rres.cachedAt) > ResolveCacheMaxAgeMutable {
635		r.Stats.IncMutableTimeouts()
636		return nil
637	}
638	if rres.err != nil && now.Sub(rres.cachedAt) > resolveCacheMaxAgeErrored {
639		r.Stats.IncErrorTimeouts()
640		return nil
641	}
642	r.Stats.IncHits()
643	rres.addKeybaseNameIfKnown(au)
644	return rres
645}
646
647func resolveDbKey(key string) DbKey {
648	return DbKey{
649		Typ: DBResolveUsernameToUID,
650		Key: NewNormalizedUsername(key).String(),
651	}
652}
653
654func (r *ResolverImpl) putToDiskCache(m MetaContext, key string, res ResolveResult) {
655	m.VLogf(VLog1, "| Resolver#putToDiskCache (attempt) %+v", res)
656	// Only cache immutable resolutions to disk
657	if res.mutable {
658		return
659	}
660	// Don't cache errors or deleted users
661	if res.err != nil || res.deleted {
662		return
663	}
664	if res.uid.IsNil() {
665		m.Warning("Mistaken UID put to disk cache")
666		if m.G().Env.GetDebug() {
667			debug.PrintStack()
668		}
669		return
670	}
671	r.Stats.IncDiskPuts()
672	if err := m.G().LocalDb.PutObj(resolveDbKey(key), nil, res.uid); err != nil {
673		m.Warning("Cannot put resolve result to disk: %s", err)
674		return
675	}
676	m.Debug("| Resolver#putToDiskCache(%s) -> %v", key, res)
677}
678
679// Put receives a copy of a ResolveResult, clears out the body
680// to avoid caching data that can go stale, and stores the result.
681func (r *ResolverImpl) putToMemCache(m MetaContext, key string, res ResolveResult) {
682	if r.cache == nil {
683		return
684	}
685	// Don't cache errors or deleted users
686	if res.err != nil || res.deleted {
687		return
688	}
689	if !res.HasPrimaryKey() {
690		m.Warning("Mistaken UID put to mem cache")
691		if m.G().Env.GetDebug() {
692			debug.PrintStack()
693		}
694		return
695	}
696	res.cachedAt = m.G().Clock().Now()
697	res.body = nil // Don't cache body
698	_ = r.cache.Set(key, &res)
699}
700
701func (r *ResolverImpl) CacheTeamResolution(m MetaContext, id keybase1.TeamID, name keybase1.TeamName) {
702	m.VLogf(VLog0, "ResolverImpl#CacheTeamResolution: %s <-> %s", id, name)
703	res := ResolveResult{
704		teamID:           id,
705		queriedByTeamID:  true,
706		resolvedTeamName: name,
707		mutable:          id.IsSubTeam(),
708	}
709	r.putToMemCache(m, fmt.Sprintf("tid:%s", id), res)
710	res.queriedByTeamID = false
711	r.putToMemCache(m, fmt.Sprintf("team:%s", name.String()), res)
712}
713
714func (r *ResolverImpl) PurgeResolveCache(m MetaContext, input string) (err error) {
715	defer m.Trace(fmt.Sprintf("Resolver#PurgeResolveCache(input = %q)", input), &err)()
716	expr, err := AssertionParseAndOnly(m.G().MakeAssertionContext(m), input)
717	if err != nil {
718		return err
719	}
720	u := FindBestIdentifyComponentURL(expr)
721	if u == nil {
722		return ResolutionError{Input: input, Msg: "Cannot find a resolvable factor"}
723	}
724
725	key := u.CacheKey()
726	err = r.cache.Delete(key)
727	if err != nil {
728		return err
729	}
730	// Since we only put to disk cache if it's a Keybase-type assertion, we
731	// only remove it in this case as well.
732	if u.IsKeybase() {
733		if err := m.G().LocalDb.Delete(resolveDbKey(key)); err != nil {
734			m.Warning("Cannot remove resolve result from disk: %s", err)
735			return err
736		}
737	}
738	return nil
739}
740
741var _ Resolver = (*ResolverImpl)(nil)
742