github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/teams/implicit.go (about)

     1  package teams
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strings"
     7  
     8  	lru "github.com/hashicorp/golang-lru"
     9  	"github.com/keybase/client/go/kbfs/tlf"
    10  	"github.com/keybase/client/go/libkb"
    11  	"github.com/keybase/client/go/protocol/keybase1"
    12  	"golang.org/x/net/context"
    13  )
    14  
    15  type implicitTeamConflict struct {
    16  	// Note this TeamID is not validated by LookupImplicitTeam. Be aware of server trust.
    17  	TeamID       keybase1.TeamID `json:"team_id"`
    18  	Generation   int             `json:"generation"`
    19  	ConflictDate string          `json:"conflict_date"`
    20  }
    21  
    22  func (i *implicitTeamConflict) parse() (*keybase1.ImplicitTeamConflictInfo, error) {
    23  	return libkb.ParseImplicitTeamDisplayNameSuffix(fmt.Sprintf("(conflicted copy %s #%d)", i.ConflictDate, i.Generation))
    24  }
    25  
    26  type implicitTeam struct {
    27  	TeamID      keybase1.TeamID        `json:"team_id"`
    28  	DisplayName string                 `json:"display_name"`
    29  	Private     bool                   `json:"is_private"`
    30  	Conflicts   []implicitTeamConflict `json:"conflicts,omitempty"`
    31  	Status      libkb.AppStatus        `json:"status"`
    32  }
    33  
    34  func (i *implicitTeam) GetAppStatus() *libkb.AppStatus {
    35  	return &i.Status
    36  }
    37  
    38  type ImplicitTeamOptions struct {
    39  	NoForceRepoll bool
    40  }
    41  
    42  // Lookup an implicit team by name like "alice,bob+bob@twitter (conflicted copy 2017-03-04 #1)"
    43  // Resolves social assertions.
    44  func LookupImplicitTeam(ctx context.Context, g *libkb.GlobalContext, displayName string, public bool, opts ImplicitTeamOptions) (
    45  	team *Team, teamName keybase1.TeamName, impTeamName keybase1.ImplicitTeamDisplayName, err error) {
    46  	team, teamName, impTeamName, _, err = LookupImplicitTeamAndConflicts(ctx, g, displayName, public, opts)
    47  	return team, teamName, impTeamName, err
    48  }
    49  
    50  // Lookup an implicit team by name like "alice,bob+bob@twitter (conflicted copy 2017-03-04 #1)"
    51  // Resolves social assertions.
    52  func LookupImplicitTeamAndConflicts(ctx context.Context, g *libkb.GlobalContext, displayName string, public bool, opts ImplicitTeamOptions) (
    53  	team *Team, teamName keybase1.TeamName, impTeamName keybase1.ImplicitTeamDisplayName, conflicts []keybase1.ImplicitTeamConflictInfo, err error) {
    54  	impName, err := ResolveImplicitTeamDisplayName(ctx, g, displayName, public)
    55  	if err != nil {
    56  		return team, teamName, impTeamName, conflicts, err
    57  	}
    58  	return lookupImplicitTeamAndConflicts(ctx, g, displayName, impName, opts)
    59  }
    60  
    61  func LookupImplicitTeamIDUntrusted(ctx context.Context, g *libkb.GlobalContext, displayName string,
    62  	public bool) (res keybase1.TeamID, err error) {
    63  	imp, _, err := loadImpteam(ctx, g, displayName, public, false /* skipCache */)
    64  	if err != nil {
    65  		return res, err
    66  	}
    67  	return imp.TeamID, nil
    68  }
    69  
    70  func loadImpteam(ctx context.Context, g *libkb.GlobalContext, displayName string, public bool, skipCache bool) (imp implicitTeam, hitCache bool, err error) {
    71  	cacheKey := impTeamCacheKey(displayName, public)
    72  	cacher := g.GetImplicitTeamCacher()
    73  	if !skipCache && cacher != nil {
    74  		if cv, ok := cacher.Get(cacheKey); ok {
    75  			if imp, ok := cv.(implicitTeam); ok {
    76  				g.Log.CDebugf(ctx, "using cached iteam")
    77  				return imp, true, nil
    78  			}
    79  			g.Log.CDebugf(ctx, "Bad element of wrong type from cache: %T", cv)
    80  		}
    81  	}
    82  	imp, err = loadImpteamFromServer(ctx, g, displayName, public)
    83  	if err != nil {
    84  		return imp, false, err
    85  	}
    86  	// If the team has any assertions skip caching.
    87  	if cacher != nil && !strings.Contains(imp.DisplayName, "@") {
    88  		cacher.Put(cacheKey, imp)
    89  	}
    90  	return imp, false, nil
    91  }
    92  
    93  func loadImpteamFromServer(ctx context.Context, g *libkb.GlobalContext, displayName string, public bool) (imp implicitTeam, err error) {
    94  	mctx := libkb.NewMetaContext(ctx, g)
    95  	arg := libkb.NewAPIArg("team/implicit")
    96  	arg.SessionType = libkb.APISessionTypeOPTIONAL
    97  	arg.Args = libkb.HTTPArgs{
    98  		"display_name": libkb.S{Val: displayName},
    99  		"public":       libkb.B{Val: public},
   100  	}
   101  	if err = mctx.G().API.GetDecode(mctx, arg, &imp); err != nil {
   102  		if aerr, ok := err.(libkb.AppStatusError); ok {
   103  			code := keybase1.StatusCode(aerr.Code)
   104  			switch code {
   105  			case keybase1.StatusCode_SCTeamReadError:
   106  				return imp, NewTeamDoesNotExistError(public, displayName)
   107  			case keybase1.StatusCode_SCTeamProvisionalCanKey, keybase1.StatusCode_SCTeamProvisionalCannotKey:
   108  				return imp, libkb.NewTeamProvisionalError(
   109  					(code == keybase1.StatusCode_SCTeamProvisionalCanKey), public, displayName)
   110  			}
   111  		}
   112  		return imp, err
   113  	}
   114  	return imp, nil
   115  }
   116  
   117  // attemptLoadImpteamAndConflits attempts to lead the implicit team with
   118  // conflict, but it might find the team but not the specific conflict if the
   119  // conflict was not in cache. This can be detected with `hitCache` return
   120  // value, and mitigated by passing skipCache=false argument.
   121  func attemptLoadImpteamAndConflict(ctx context.Context, g *libkb.GlobalContext, impTeamName keybase1.ImplicitTeamDisplayName,
   122  	nameWithoutConflict string, preResolveDisplayName string, skipCache bool) (conflicts []keybase1.ImplicitTeamConflictInfo, teamID keybase1.TeamID, hitCache bool, err error) {
   123  
   124  	defer g.CTrace(ctx,
   125  		fmt.Sprintf("attemptLoadImpteamAndConflict(impName=%q,woConflict=%q,preResolve=%q,skipCache=%t)", impTeamName, nameWithoutConflict, preResolveDisplayName, skipCache),
   126  		&err)()
   127  	imp, hitCache, err := loadImpteam(ctx, g, nameWithoutConflict, impTeamName.IsPublic, skipCache)
   128  	if err != nil {
   129  		return conflicts, teamID, hitCache, err
   130  	}
   131  	if len(imp.Conflicts) > 0 {
   132  		g.Log.CDebugf(ctx, "LookupImplicitTeam found %v conflicts", len(imp.Conflicts))
   133  	}
   134  	// We will use this team. Changed later if we selected a conflict.
   135  	var foundSelectedConflict bool
   136  	teamID = imp.TeamID
   137  	// We still need to iterate over Conflicts because we are returning parsed
   138  	// conflict list. So even if caller is not requesting a conflict team, go
   139  	// through this loop.
   140  	for i, conflict := range imp.Conflicts {
   141  		g.Log.CDebugf(ctx, "| checking conflict: %+v (iter %d)", conflict, i)
   142  		conflictInfo, err := conflict.parse()
   143  		if err != nil {
   144  			// warn, don't fail
   145  			g.Log.CDebugf(ctx, "LookupImplicitTeam got conflict suffix: %v", err)
   146  			continue
   147  		}
   148  		if conflictInfo == nil {
   149  			g.Log.CDebugf(ctx, "| got unexpected nil conflictInfo (iter %d)", i)
   150  			continue
   151  		}
   152  		conflicts = append(conflicts, *conflictInfo)
   153  
   154  		g.Log.CDebugf(ctx, "| parsed conflict into conflictInfo: %+v", *conflictInfo)
   155  
   156  		if impTeamName.ConflictInfo != nil {
   157  			match := libkb.FormatImplicitTeamDisplayNameSuffix(*impTeamName.ConflictInfo) == libkb.FormatImplicitTeamDisplayNameSuffix(*conflictInfo)
   158  			if match {
   159  				teamID = conflict.TeamID
   160  				foundSelectedConflict = true
   161  				g.Log.CDebugf(ctx, "| found conflict suffix match: %v", teamID)
   162  			} else {
   163  				g.Log.CDebugf(ctx, "| conflict suffix didn't match (teamID %v)", conflict.TeamID)
   164  			}
   165  		}
   166  	}
   167  	if impTeamName.ConflictInfo != nil && !foundSelectedConflict {
   168  		// We got the team but didn't find the specific conflict requested.
   169  		return conflicts, teamID, hitCache, NewTeamDoesNotExistError(
   170  			impTeamName.IsPublic, "could not find team with suffix: %v", preResolveDisplayName)
   171  	}
   172  	return conflicts, teamID, hitCache, nil
   173  }
   174  
   175  // Lookup an implicit team by name like "alice,bob+bob@twitter (conflicted copy 2017-03-04 #1)"
   176  // Does not resolve social assertions.
   177  // preResolveDisplayName is used for logging and errors
   178  func lookupImplicitTeamAndConflicts(ctx context.Context, g *libkb.GlobalContext,
   179  	preResolveDisplayName string, impTeamNameInput keybase1.ImplicitTeamDisplayName, opts ImplicitTeamOptions) (
   180  	team *Team, teamName keybase1.TeamName, impTeamName keybase1.ImplicitTeamDisplayName, conflicts []keybase1.ImplicitTeamConflictInfo, err error) {
   181  
   182  	defer g.CTrace(ctx, fmt.Sprintf("lookupImplicitTeamAndConflicts(%v,opts=%+v)", preResolveDisplayName, opts), &err)()
   183  
   184  	impTeamName = impTeamNameInput
   185  
   186  	// Use a copy without the conflict info to hit the api endpoint
   187  	impTeamNameWithoutConflict := impTeamName
   188  	impTeamNameWithoutConflict.ConflictInfo = nil
   189  	lookupNameWithoutConflict, err := FormatImplicitTeamDisplayName(ctx, g, impTeamNameWithoutConflict)
   190  	if err != nil {
   191  		return team, teamName, impTeamName, conflicts, err
   192  	}
   193  
   194  	// Try the load first -- once with a cache, and once nameWithoutConflict.
   195  	var teamID keybase1.TeamID
   196  	var hitCache bool
   197  	conflicts, teamID, hitCache, err = attemptLoadImpteamAndConflict(ctx, g, impTeamName, lookupNameWithoutConflict, preResolveDisplayName, false /* skipCache */)
   198  	if _, dne := err.(TeamDoesNotExistError); dne && hitCache {
   199  		// We are looking for conflict team that we didn't find. Maybe we have the team
   200  		// cached from before another team was resolved and this team became conflicted.
   201  		// Try again skipping cache.
   202  		g.Log.CDebugf(ctx, "attemptLoadImpteamAndConflict failed to load team %q from cache, trying again skipping cache", preResolveDisplayName)
   203  		conflicts, teamID, _, err = attemptLoadImpteamAndConflict(ctx, g, impTeamName, lookupNameWithoutConflict, preResolveDisplayName, true /* skipCache */)
   204  	}
   205  	if err != nil {
   206  		return team, teamName, impTeamName, conflicts, err
   207  	}
   208  
   209  	team, err = Load(ctx, g, keybase1.LoadTeamArg{
   210  		ID:          teamID,
   211  		Public:      impTeamName.IsPublic,
   212  		ForceRepoll: !opts.NoForceRepoll,
   213  	})
   214  	if err != nil {
   215  		return team, teamName, impTeamName, conflicts, err
   216  	}
   217  
   218  	// Check the display names. This is how we make sure the server returned a team with the right members.
   219  	teamDisplayName, err := team.ImplicitTeamDisplayNameString(ctx)
   220  	if err != nil {
   221  		return team, teamName, impTeamName, conflicts, err
   222  	}
   223  	referenceImpName, err := FormatImplicitTeamDisplayName(ctx, g, impTeamName)
   224  	if err != nil {
   225  		return team, teamName, impTeamName, conflicts, err
   226  	}
   227  	if teamDisplayName != referenceImpName {
   228  		return team, teamName, impTeamName, conflicts, fmt.Errorf("implicit team name mismatch: %s != %s",
   229  			teamDisplayName, referenceImpName)
   230  	}
   231  	if team.IsPublic() != impTeamName.IsPublic {
   232  		return team, teamName, impTeamName, conflicts, fmt.Errorf("implicit team public-ness mismatch: %v != %v", team.IsPublic(), impTeamName.IsPublic)
   233  	}
   234  
   235  	return team, team.Name(), impTeamName, conflicts, nil
   236  }
   237  
   238  func isDupImplicitTeamError(err error) bool {
   239  	if err != nil {
   240  		if aerr, ok := err.(libkb.AppStatusError); ok {
   241  			code := keybase1.StatusCode(aerr.Code)
   242  			switch code {
   243  			case keybase1.StatusCode_SCTeamImplicitDuplicate:
   244  				return true
   245  			default:
   246  				// Nothing to do for other codes.
   247  			}
   248  		}
   249  	}
   250  	return false
   251  }
   252  
   253  func assertIsDisplayNameNormalized(displayName keybase1.ImplicitTeamDisplayName) error {
   254  	var errs []error
   255  	for _, userSet := range []keybase1.ImplicitTeamUserSet{displayName.Writers, displayName.Readers} {
   256  		for _, username := range userSet.KeybaseUsers {
   257  			if !libkb.IsLowercase(username) {
   258  				errs = append(errs, fmt.Errorf("Keybase username %q has mixed case", username))
   259  			}
   260  		}
   261  		for _, assertion := range userSet.UnresolvedUsers {
   262  			if !libkb.IsLowercase(assertion.User) {
   263  				errs = append(errs, fmt.Errorf("User %q in assertion %q has mixed case", assertion.User, assertion.String()))
   264  			}
   265  		}
   266  	}
   267  	return libkb.CombineErrors(errs...)
   268  }
   269  
   270  // LookupOrCreateImplicitTeam by name like "alice,bob+bob@twitter (conflicted copy 2017-03-04 #1)"
   271  // Resolves social assertions.
   272  func LookupOrCreateImplicitTeam(ctx context.Context, g *libkb.GlobalContext, displayName string, public bool) (res *Team, teamName keybase1.TeamName, impTeamName keybase1.ImplicitTeamDisplayName, err error) {
   273  	ctx = libkb.WithLogTag(ctx, "LOCIT")
   274  	defer g.CTrace(ctx, fmt.Sprintf("LookupOrCreateImplicitTeam(%v)", displayName),
   275  		&err)()
   276  	lookupName, err := ResolveImplicitTeamDisplayName(ctx, g, displayName, public)
   277  	if err != nil {
   278  		return res, teamName, impTeamName, err
   279  	}
   280  
   281  	if err := assertIsDisplayNameNormalized(lookupName); err != nil {
   282  		// Do not allow display names with mixed letter case - while it's legal
   283  		// to create them, it will not be possible to load them because API
   284  		// server always downcases during normalization.
   285  		return res, teamName, impTeamName, fmt.Errorf("Display name is not normalized: %s", err)
   286  	}
   287  
   288  	res, teamName, impTeamName, _, err = lookupImplicitTeamAndConflicts(ctx, g, displayName, lookupName, ImplicitTeamOptions{})
   289  	if err != nil {
   290  		if _, ok := err.(TeamDoesNotExistError); ok {
   291  			if lookupName.ConflictInfo != nil {
   292  				// Don't create it if a conflict is specified.
   293  				// Unlikely a caller would know the conflict info if it didn't exist.
   294  				return res, teamName, impTeamName, err
   295  			}
   296  			// If the team does not exist, then let's create it
   297  			impTeamName = lookupName
   298  			var teamID keybase1.TeamID
   299  			teamID, teamName, err = CreateImplicitTeam(ctx, g, impTeamName)
   300  			if err != nil {
   301  				if isDupImplicitTeamError(err) {
   302  					g.Log.CDebugf(ctx, "LookupOrCreateImplicitTeam: duplicate team, trying to lookup again: err: %s", err)
   303  					res, teamName, impTeamName, _, err = lookupImplicitTeamAndConflicts(ctx, g, displayName,
   304  						lookupName, ImplicitTeamOptions{})
   305  				}
   306  				return res, teamName, impTeamName, err
   307  			}
   308  			res, err = Load(ctx, g, keybase1.LoadTeamArg{
   309  				ID:          teamID,
   310  				Public:      impTeamName.IsPublic,
   311  				ForceRepoll: true,
   312  				AuditMode:   keybase1.AuditMode_JUST_CREATED,
   313  			})
   314  			return res, teamName, impTeamName, err
   315  		}
   316  		return res, teamName, impTeamName, err
   317  	}
   318  	return res, teamName, impTeamName, nil
   319  }
   320  
   321  func FormatImplicitTeamDisplayName(ctx context.Context, g *libkb.GlobalContext, impTeamName keybase1.ImplicitTeamDisplayName) (string, error) {
   322  	return formatImplicitTeamDisplayNameCommon(ctx, g, impTeamName, nil)
   323  }
   324  
   325  // Format an implicit display name, but order the specified username first in each of the writer and reader lists if it appears.
   326  func FormatImplicitTeamDisplayNameWithUserFront(ctx context.Context, g *libkb.GlobalContext, impTeamName keybase1.ImplicitTeamDisplayName, frontName libkb.NormalizedUsername) (string, error) {
   327  	return formatImplicitTeamDisplayNameCommon(ctx, g, impTeamName, &frontName)
   328  }
   329  
   330  func formatImplicitTeamDisplayNameCommon(ctx context.Context, g *libkb.GlobalContext, impTeamName keybase1.ImplicitTeamDisplayName, optionalFrontName *libkb.NormalizedUsername) (string, error) {
   331  	writerNames := make([]string, 0, len(impTeamName.Writers.KeybaseUsers)+len(impTeamName.Writers.UnresolvedUsers))
   332  	writerNames = append(writerNames, impTeamName.Writers.KeybaseUsers...)
   333  	for _, u := range impTeamName.Writers.UnresolvedUsers {
   334  		writerNames = append(writerNames, u.String())
   335  	}
   336  
   337  	if optionalFrontName == nil {
   338  		sort.Strings(writerNames)
   339  	} else {
   340  		sortStringsFront(writerNames, optionalFrontName.String())
   341  	}
   342  
   343  	readerNames := make([]string, 0, len(impTeamName.Readers.KeybaseUsers)+len(impTeamName.Readers.UnresolvedUsers))
   344  	readerNames = append(readerNames, impTeamName.Readers.KeybaseUsers...)
   345  	for _, u := range impTeamName.Readers.UnresolvedUsers {
   346  		readerNames = append(readerNames, u.String())
   347  	}
   348  	if optionalFrontName == nil {
   349  		sort.Strings(readerNames)
   350  	} else {
   351  		sortStringsFront(readerNames, optionalFrontName.String())
   352  	}
   353  
   354  	var suffix string
   355  	if impTeamName.ConflictInfo.IsConflict() {
   356  		suffix = libkb.FormatImplicitTeamDisplayNameSuffix(*impTeamName.ConflictInfo)
   357  	}
   358  
   359  	if len(writerNames) == 0 {
   360  		return "", fmt.Errorf("invalid implicit team name: no writers")
   361  	}
   362  
   363  	return tlf.NormalizeNamesInTLF(libkb.NewMetaContext(ctx, g), writerNames, readerNames, suffix)
   364  }
   365  
   366  // Sort a list of strings but order `front` in front IF it appears.
   367  func sortStringsFront(ss []string, front string) {
   368  	sort.Slice(ss, func(i, j int) bool {
   369  		a := ss[i]
   370  		b := ss[j]
   371  		if a == front {
   372  			return true
   373  		}
   374  		if b == front {
   375  			return false
   376  		}
   377  		return a < b
   378  	})
   379  }
   380  
   381  func impTeamCacheKey(displayName string, public bool) string {
   382  	return fmt.Sprintf("%s-%v", displayName, public)
   383  }
   384  
   385  type implicitTeamCache struct {
   386  	cache *lru.Cache
   387  }
   388  
   389  func newImplicitTeamCache(g *libkb.GlobalContext) *implicitTeamCache {
   390  	cache, err := lru.New(libkb.ImplicitTeamCacheSize)
   391  	if err != nil {
   392  		panic(err)
   393  	}
   394  	return &implicitTeamCache{
   395  		cache: cache,
   396  	}
   397  }
   398  
   399  func (i *implicitTeamCache) Get(key interface{}) (interface{}, bool) {
   400  	return i.cache.Get(key)
   401  }
   402  
   403  func (i *implicitTeamCache) Put(key, value interface{}) bool {
   404  	return i.cache.Add(key, value)
   405  }
   406  
   407  func (i *implicitTeamCache) OnLogout(m libkb.MetaContext) error {
   408  	i.cache.Purge()
   409  	return nil
   410  }
   411  
   412  func (i *implicitTeamCache) OnDbNuke(m libkb.MetaContext) error {
   413  	i.cache.Purge()
   414  	return nil
   415  }
   416  
   417  var _ libkb.MemLRUer = &implicitTeamCache{}
   418  
   419  func NewImplicitTeamCacheAndInstall(g *libkb.GlobalContext) {
   420  	cache := newImplicitTeamCache(g)
   421  	g.SetImplicitTeamCacher(cache)
   422  	g.AddLogoutHook(cache, "implicitTeamCache")
   423  	g.AddDbNukeHook(cache, "implicitTeamCache")
   424  }