github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/teams/resolve.go (about)

     1  package teams
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  
     8  	"golang.org/x/sync/errgroup"
     9  
    10  	"github.com/keybase/client/go/engine"
    11  	"github.com/keybase/client/go/externals"
    12  	"github.com/keybase/client/go/libkb"
    13  	keybase1 "github.com/keybase/client/go/protocol/keybase1"
    14  )
    15  
    16  // ResolveIDToName takes a team ID and resolves it to a name.
    17  // It can use server-assist but always cryptographically checks the result.
    18  func ResolveIDToName(ctx context.Context, g *libkb.GlobalContext, id keybase1.TeamID) (name keybase1.TeamName, err error) {
    19  	return resolveIDToName(ctx, g, id, false)
    20  }
    21  
    22  // ResolveIDToNameForceRefresh is like ResolveIDToName but forces a refresh of
    23  // the FTL cache.
    24  func ResolveIDToNameForceRefresh(ctx context.Context, g *libkb.GlobalContext, id keybase1.TeamID) (name keybase1.TeamName, err error) {
    25  	return resolveIDToName(ctx, g, id, true)
    26  }
    27  
    28  func resolveIDToName(ctx context.Context, g *libkb.GlobalContext, id keybase1.TeamID, forceRefresh bool) (name keybase1.TeamName, err error) {
    29  	m := libkb.NewMetaContext(ctx, g)
    30  	rres := g.Resolver.ResolveFullExpression(m, fmt.Sprintf("tid:%s", id))
    31  	if err = rres.GetError(); err != nil {
    32  		return keybase1.TeamName{}, err
    33  	}
    34  	name = rres.GetTeamName()
    35  	if err = g.GetFastTeamLoader().VerifyTeamName(m, id, name, forceRefresh); err != nil {
    36  		return keybase1.TeamName{}, err
    37  	}
    38  
    39  	return name, nil
    40  }
    41  
    42  // ResolveNameToID takes a team name and resolves it to a team ID.
    43  // It can use server-assist but always cryptographically checks the result.
    44  func ResolveNameToID(ctx context.Context, g *libkb.GlobalContext, name keybase1.TeamName) (id keybase1.TeamID, err error) {
    45  	return resolveNameToID(ctx, g, name, false)
    46  }
    47  
    48  // ResolveNameToIDForceRefresh is just like ResolveNameToID but it forces a refresh.
    49  func ResolveNameToIDForceRefresh(ctx context.Context, g *libkb.GlobalContext, name keybase1.TeamName) (id keybase1.TeamID, err error) {
    50  	return resolveNameToID(ctx, g, name, true)
    51  }
    52  
    53  func resolveNameToID(ctx context.Context, g *libkb.GlobalContext, name keybase1.TeamName, forceRefresh bool) (id keybase1.TeamID, err error) {
    54  	m := libkb.NewMetaContext(ctx, g)
    55  	rres := g.Resolver.ResolveFullExpression(m, fmt.Sprintf("team:%s", name))
    56  	if err = rres.GetError(); err != nil {
    57  		return keybase1.TeamID(""), err
    58  	}
    59  	id = rres.GetTeamID()
    60  	if err = g.GetFastTeamLoader().VerifyTeamName(m, id, name, forceRefresh); err != nil {
    61  		return keybase1.TeamID(""), err
    62  	}
    63  	return id, nil
    64  }
    65  
    66  func PurgeResolverTeamID(ctx context.Context, g *libkb.GlobalContext, teamID keybase1.TeamID) error {
    67  	m := libkb.NewMetaContext(ctx, g)
    68  	return g.Resolver.PurgeResolveCache(m, fmt.Sprintf("tid:%s", teamID))
    69  }
    70  
    71  // Resolve assertions in an implicit team display name and verify the result.
    72  // Resolve an implicit team name with assertions like "alice,bob+bob@twitter#char (conflicted copy 2017-03-04 #1)"
    73  // Into "alice,bob#char (conflicted copy 2017-03-04 #1)"
    74  // The input can contain compound assertions, but if compound assertions are left unresolved, an error will be returned.
    75  func ResolveImplicitTeamDisplayName(ctx context.Context, g *libkb.GlobalContext,
    76  	name string, public bool) (res keybase1.ImplicitTeamDisplayName, err error) {
    77  
    78  	defer g.CTrace(ctx, fmt.Sprintf("ResolveImplicitTeamDisplayName(%v, public:%v)", name, public), &err)()
    79  
    80  	split1 := strings.SplitN(name, " ", 2) // split1: [assertions, ?conflict]
    81  	assertions := split1[0]
    82  	var suffix string
    83  	if len(split1) > 1 {
    84  		suffix = split1[1]
    85  	}
    86  
    87  	writerAssertions, readerAssertions, err := externals.ParseAssertionsWithReaders(libkb.NewMetaContext(ctx, g), assertions)
    88  	if err != nil {
    89  		return res, err
    90  	}
    91  
    92  	res = keybase1.ImplicitTeamDisplayName{
    93  		IsPublic: public,
    94  	}
    95  	if len(suffix) > 0 {
    96  		res.ConflictInfo, err = libkb.ParseImplicitTeamDisplayNameSuffix(suffix)
    97  		if err != nil {
    98  			return res, err
    99  		}
   100  	}
   101  
   102  	var resolvedAssertions []libkb.ResolvedAssertion
   103  	if err = ResolveImplicitTeamSetUntrusted(ctx, g, writerAssertions, &res.Writers, &resolvedAssertions); err != nil {
   104  		return res, err
   105  	}
   106  	if err = ResolveImplicitTeamSetUntrusted(ctx, g, readerAssertions, &res.Readers, &resolvedAssertions); err != nil {
   107  		return res, err
   108  	}
   109  
   110  	deduplicateImplicitTeamDisplayName(&res)
   111  
   112  	// errgroup collects errors and returns the first non-nil.
   113  	// subctx is canceled when the group finishes.
   114  	group, subctx := errgroup.WithContext(ctx)
   115  
   116  	// Identify everyone who resolved in parallel, checking that they match their resolved UID and original assertions.
   117  	for _, resolvedAssertion := range resolvedAssertions {
   118  		resolvedAssertion := resolvedAssertion // https://golang.org/doc/faq#closures_and_goroutines
   119  		group.Go(func() error {
   120  			return verifyResolveResult(subctx, g, resolvedAssertion)
   121  		})
   122  	}
   123  
   124  	err = group.Wait()
   125  	return res, err
   126  }
   127  
   128  // preventTeamCreationOnError checks if an error coming from resolver should
   129  // prevent us from creating a team. We don't want a team where we don't know if
   130  // SBS user is resolvable but we just were unable to get the answer.
   131  func shouldPreventTeamCreation(err error) bool {
   132  	if resErr, ok := err.(libkb.ResolutionError); ok {
   133  		switch resErr.Kind {
   134  		case libkb.ResolutionErrorRateLimited, libkb.ResolutionErrorInvalidInput, libkb.ResolutionErrorRequestFailed:
   135  			return true
   136  		}
   137  	}
   138  	return false
   139  }
   140  
   141  // Try to resolve implicit team members.
   142  // Modifies the arguments `resSet` and appends to `resolvedAssertions`.
   143  // For each assertion in `sourceAssertions`, try to resolve them.
   144  //
   145  //	If they resolve, add the username to `resSet` and the assertion to `resolvedAssertions`.
   146  //	If they don't resolve, add the SocialAssertion to `resSet`, but nothing to `resolvedAssertions`.
   147  func ResolveImplicitTeamSetUntrusted(ctx context.Context, g *libkb.GlobalContext,
   148  	sourceAssertions []libkb.AssertionExpression, resSet *keybase1.ImplicitTeamUserSet, resolvedAssertions *[]libkb.ResolvedAssertion) error {
   149  
   150  	m := libkb.NewMetaContext(ctx, g)
   151  
   152  	for _, expr := range sourceAssertions {
   153  		u, resolveRes, err := g.Resolver.ResolveUser(m, expr.String())
   154  		if err != nil {
   155  			// Resolution failed. Could still be an SBS assertion.
   156  			if shouldPreventTeamCreation(err) {
   157  				// but if we are not sure, better to bail out
   158  				return err
   159  			}
   160  			sa, err := expr.ToSocialAssertion()
   161  			if err != nil {
   162  				// Could not convert to a social assertion.
   163  				// This could be because it is a compound assertion, which we do not support when SBS.
   164  				// Or it could be because it's a team assertion or something weird like that.
   165  				return libkb.ResolutionError{Input: expr.String(), Msg: "unknown user assertion",
   166  					Kind: libkb.ResolutionErrorNotFound}
   167  			}
   168  			resSet.UnresolvedUsers = append(resSet.UnresolvedUsers, sa)
   169  		} else {
   170  			// Resolution succeeded
   171  			resSet.KeybaseUsers = append(resSet.KeybaseUsers, u.Username)
   172  			// Append the resolvee and assertion to resolvedAssertions, in case we identify later.
   173  			*resolvedAssertions = append(*resolvedAssertions, libkb.ResolvedAssertion{
   174  				UID:           u.Uid,
   175  				Assertion:     expr,
   176  				ResolveResult: resolveRes,
   177  			})
   178  		}
   179  	}
   180  	return nil
   181  }
   182  
   183  // Verify using Identify that a UID matches an assertion.
   184  func verifyResolveResult(ctx context.Context, g *libkb.GlobalContext, resolvedAssertion libkb.ResolvedAssertion) (err error) {
   185  
   186  	defer g.CTrace(ctx, fmt.Sprintf("verifyResolveResult ID user [%s] %s", resolvedAssertion.UID, resolvedAssertion.Assertion.String()),
   187  		&err)()
   188  
   189  	if resolvedAssertion.ResolveResult.WasKBAssertion() {
   190  		// The resolver does not use server-trust for these sorts of assertions.
   191  		// So early out to avoid the performance cost of a full identify.
   192  		return nil
   193  	}
   194  
   195  	if resolvedAssertion.ResolveResult.IsServerTrust() {
   196  		g.Log.CDebugf(ctx, "Trusting the server on assertion: %q (server trust - no way for clients to verify)", resolvedAssertion.Assertion.String())
   197  		return nil
   198  	}
   199  
   200  	id2arg := keybase1.Identify2Arg{
   201  		Uid:           resolvedAssertion.UID,
   202  		UserAssertion: resolvedAssertion.Assertion.String(),
   203  		CanSuppressUI: true,
   204  		// Use CHAT_GUI to avoid tracker popups and DO externals checks.
   205  		IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_GUI,
   206  	}
   207  
   208  	uis := libkb.UIs{
   209  		// Send a nil IdentifyUI, this IdentifyBehavior should not use it anyway.
   210  		IdentifyUI: nil,
   211  	}
   212  
   213  	eng := engine.NewIdentify2WithUID(g, &id2arg)
   214  	m := libkb.NewMetaContext(ctx, g).WithUIs(uis)
   215  	err = engine.RunEngine2(m, eng)
   216  	if err != nil {
   217  		idRes, _ := eng.Result(m)
   218  		m.Debug("identify failed (IDres %v, TrackBreaks %v): %v", idRes != nil, idRes != nil && idRes.TrackBreaks != nil, err)
   219  	}
   220  	return err
   221  }
   222  
   223  // Remove duplicates from a team display name.
   224  // Does not do any resolution nor resolution of UIDs.
   225  // "alice" -> "alice"
   226  // "alice,bob,alice" -> "alice"
   227  // "alice#alice" -> "alice"
   228  // "alice,bob#alice#char" -> "alice,bob#char"
   229  func deduplicateImplicitTeamDisplayName(name *keybase1.ImplicitTeamDisplayName) {
   230  	seen := make(map[string]bool)
   231  
   232  	unseen := func(idx string) bool {
   233  		seenBefore := seen[idx]
   234  		seen[idx] = true
   235  		return !seenBefore
   236  	}
   237  
   238  	var writers keybase1.ImplicitTeamUserSet
   239  	var readers keybase1.ImplicitTeamUserSet
   240  
   241  	for _, u := range name.Writers.KeybaseUsers {
   242  		if unseen(u) {
   243  			writers.KeybaseUsers = append(writers.KeybaseUsers, u)
   244  		}
   245  	}
   246  	for _, u := range name.Writers.UnresolvedUsers {
   247  		if unseen(u.String()) {
   248  			writers.UnresolvedUsers = append(writers.UnresolvedUsers, u)
   249  		}
   250  	}
   251  
   252  	for _, u := range name.Readers.KeybaseUsers {
   253  		if unseen(u) {
   254  			readers.KeybaseUsers = append(readers.KeybaseUsers, u)
   255  		}
   256  	}
   257  	for _, u := range name.Readers.UnresolvedUsers {
   258  		if unseen(u.String()) {
   259  			readers.UnresolvedUsers = append(readers.UnresolvedUsers, u)
   260  		}
   261  	}
   262  
   263  	name.Writers = writers
   264  	name.Readers = readers
   265  }