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 }