github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/tlfhandle/identify_util.go (about) 1 // Copyright 2016 Keybase Inc. All rights reserved. 2 // Use of this source code is governed by a BSD 3 // license that can be found in the LICENSE file. 4 5 package tlfhandle 6 7 import ( 8 "errors" 9 "fmt" 10 "sync" 11 12 "github.com/keybase/client/go/kbfs/idutil" 13 "github.com/keybase/client/go/kbfs/libcontext" 14 "github.com/keybase/client/go/kbfs/tlf" 15 kbname "github.com/keybase/client/go/kbun" 16 "github.com/keybase/client/go/libkb" 17 "github.com/keybase/client/go/protocol/keybase1" 18 "golang.org/x/net/context" 19 "golang.org/x/sync/errgroup" 20 ) 21 22 // ExtendedIdentify is a struct to track the behavior and results of 23 // an identify. 24 type ExtendedIdentify struct { 25 Behavior keybase1.TLFIdentifyBehavior 26 27 // lock guards userBreaks and tlfBreaks 28 lock sync.Mutex 29 userBreaks chan keybase1.TLFIdentifyFailure 30 tlfBreaks *keybase1.TLFBreak 31 } 32 33 // UserBreak should be called when an identify call for a user has 34 // completed, and may (or may not) contain breaks. 35 func (ei *ExtendedIdentify) UserBreak( 36 ctx context.Context, username kbname.NormalizedUsername, uid keybase1.UID, 37 breaks *keybase1.IdentifyTrackBreaks) { 38 if ei.userBreaks == nil { 39 return 40 } 41 42 select { 43 case ei.userBreaks <- keybase1.TLFIdentifyFailure{ 44 Breaks: breaks, 45 User: keybase1.User{ 46 Uid: uid, 47 Username: string(username), 48 }, 49 }: 50 case <-ctx.Done(): 51 } 52 } 53 54 // TeamBreak should be called when an identify call for a team has 55 // completed, and may (or may not) contain breaks. 56 func (ei *ExtendedIdentify) TeamBreak( 57 ctx context.Context, teamID keybase1.TeamID, 58 breaks *keybase1.IdentifyTrackBreaks) { 59 if ei.userBreaks == nil { 60 return 61 } 62 63 if breaks != nil && (len(breaks.Keys) != 0 || len(breaks.Proofs) != 0) { 64 panic(fmt.Sprintf("Unexpected team %s breaks: %v", teamID, breaks)) 65 } 66 67 // Otherwise just send an empty message to close the loop. 68 select { 69 case ei.userBreaks <- keybase1.TLFIdentifyFailure{ 70 Breaks: nil, 71 User: keybase1.User{}, 72 }: 73 case <-ctx.Done(): 74 } 75 } 76 77 // OnError is called when the identify process has encountered a hard 78 // error. 79 func (ei *ExtendedIdentify) OnError(ctx context.Context) { 80 if ei.userBreaks == nil { 81 return 82 } 83 84 // The identify got an error, so just send a nil breaks list so 85 // that the goroutine waiting on the breaks can finish and the 86 // error can be returned. 87 select { 88 case ei.userBreaks <- keybase1.TLFIdentifyFailure{ 89 Breaks: nil, 90 User: keybase1.User{}, 91 }: 92 case <-ctx.Done(): 93 } 94 } 95 96 func (ei *ExtendedIdentify) makeTlfBreaksIfNeeded( 97 ctx context.Context, numUserInTlf int) error { 98 if ei.userBreaks == nil { 99 return nil 100 } 101 102 ei.lock.Lock() 103 defer ei.lock.Unlock() 104 105 b := &keybase1.TLFBreak{} 106 for i := 0; i < numUserInTlf; i++ { 107 select { 108 case ub, ok := <-ei.userBreaks: 109 if !ok { 110 return errors.New("makeTlfBreaksIfNeeded called on ExtendedIdentify" + 111 " with closed userBreaks channel.") 112 } 113 if ub.Breaks != nil { 114 b.Breaks = append(b.Breaks, ub) 115 } 116 case <-ctx.Done(): 117 return ctx.Err() 118 } 119 } 120 ei.tlfBreaks = b 121 122 return nil 123 } 124 125 // GetTlfBreakAndClose returns a keybase1.TLFBreak. This should only 126 // be called for behavior.WarningInsteadOfErrorOnBrokenTracks() == 127 // true, and after makeTlfBreaksIfNeeded is called, to make sure user 128 // proof breaks get populated in GUI mode. 129 // 130 // If called otherwise, we don't panic here anymore, since we can't 131 // panic on nil ei.tlfBreaks. The reason is if a previous successful 132 // identify has already happened recently, it could cause this 133 // identify to be skipped, which means ei.tlfBreaks is never 134 // populated. In this case, it's safe to return an empty 135 // keybase1.TLFBreak. 136 func (ei *ExtendedIdentify) GetTlfBreakAndClose() keybase1.TLFBreak { 137 ei.lock.Lock() 138 defer ei.lock.Unlock() 139 140 if ei.userBreaks != nil { 141 close(ei.userBreaks) 142 ei.userBreaks = nil 143 } 144 if ei.tlfBreaks != nil { 145 return *ei.tlfBreaks 146 } 147 148 return keybase1.TLFBreak{} 149 } 150 151 // ctxExtendedIdentifyKeyType is a type for the context key for using 152 // ExtendedIdentify 153 type ctxExtendedIdentifyKeyType int 154 155 const ( 156 // ctxExtendedIdentifyKeyType is a context key for using ExtendedIdentify 157 ctxExtendedIdentifyKey ctxExtendedIdentifyKeyType = iota 158 ) 159 160 // ExtendedIdentifyAlreadyExists is returned when MakeExtendedIdentify is 161 // called on a context already with ExtendedIdentify. 162 type ExtendedIdentifyAlreadyExists struct{} 163 164 func (e ExtendedIdentifyAlreadyExists) Error() string { 165 return "extendedIdentify already exists" 166 } 167 168 // MakeExtendedIdentify populates a context with an ExtendedIdentify directive. 169 func MakeExtendedIdentify(ctx context.Context, 170 behavior keybase1.TLFIdentifyBehavior) (context.Context, error) { 171 if _, ok := ctx.Value(ctxExtendedIdentifyKey).(*ExtendedIdentify); ok { 172 return nil, ExtendedIdentifyAlreadyExists{} 173 } 174 175 if !behavior.WarningInsteadOfErrorOnBrokenTracks() { 176 return libcontext.NewContextReplayable( 177 ctx, func(ctx context.Context) context.Context { 178 return context.WithValue( 179 ctx, ctxExtendedIdentifyKey, &ExtendedIdentify{ 180 Behavior: behavior, 181 }) 182 }), nil 183 } 184 185 ch := make(chan keybase1.TLFIdentifyFailure) 186 return libcontext.NewContextReplayable( 187 ctx, func(ctx context.Context) context.Context { 188 return context.WithValue( 189 ctx, ctxExtendedIdentifyKey, &ExtendedIdentify{ 190 Behavior: behavior, 191 userBreaks: ch, 192 }) 193 }), nil 194 } 195 196 // GetExtendedIdentify returns the extended identify info associated 197 // with the given context. 198 func GetExtendedIdentify(ctx context.Context) (ei *ExtendedIdentify) { 199 if ei, ok := ctx.Value(ctxExtendedIdentifyKey).(*ExtendedIdentify); ok { 200 return ei 201 } 202 return &ExtendedIdentify{ 203 Behavior: keybase1.TLFIdentifyBehavior_DEFAULT_KBFS, 204 } 205 } 206 207 // identifyUID performs identify based only on UID. It should be 208 // used only if the username is not known - as e.g. when rekeying. 209 func identifyUID(ctx context.Context, nug idutil.NormalizedUsernameGetter, 210 identifier idutil.Identifier, id keybase1.UserOrTeamID, t tlf.Type, 211 offline keybase1.OfflineAvailability) error { 212 name, err := nug.GetNormalizedUsername(ctx, id, offline) 213 if err != nil { 214 return err 215 } 216 return identifyUser(ctx, nug, identifier, name, id, t, offline) 217 } 218 219 // identifyUser is the preferred way to run identifies. 220 func identifyUser(ctx context.Context, nug idutil.NormalizedUsernameGetter, 221 identifier idutil.Identifier, name kbname.NormalizedUsername, 222 id keybase1.UserOrTeamID, t tlf.Type, 223 offline keybase1.OfflineAvailability) error { 224 // Check to see if identify should be skipped altogether. 225 ei := GetExtendedIdentify(ctx) 226 if ei.Behavior == keybase1.TLFIdentifyBehavior_CHAT_SKIP { 227 return nil 228 } 229 230 var reason string 231 nameAssertion := name.String() 232 isImplicit := false 233 switch t { 234 case tlf.Public: 235 if id.IsTeam() { 236 isImplicit = true 237 } 238 reason = "You accessed a public folder." 239 case tlf.Private: 240 if id.IsTeam() { 241 isImplicit = true 242 reason = fmt.Sprintf( 243 "You accessed a folder for private team %s.", nameAssertion) 244 } else { 245 reason = fmt.Sprintf( 246 "You accessed a private folder with %s.", nameAssertion) 247 } 248 case tlf.SingleTeam: 249 reason = fmt.Sprintf( 250 "You accessed a folder for private team %s.", nameAssertion) 251 nameAssertion = "team:" + nameAssertion 252 } 253 var resultName kbname.NormalizedUsername 254 var resultID keybase1.UserOrTeamID 255 if isImplicit { 256 assertions, extensionSuffix, err := tlf.SplitExtension(name.String()) 257 if err != nil { 258 return err 259 } 260 iteamInfo, err := identifier.IdentifyImplicitTeam( 261 ctx, assertions, extensionSuffix, t, reason, offline) 262 if err != nil { 263 return err 264 } 265 resultName = iteamInfo.Name 266 resultID = iteamInfo.TID.AsUserOrTeam() 267 } else { 268 var err error 269 resultName, resultID, err = 270 identifier.Identify(ctx, nameAssertion, reason, offline) 271 if err != nil { 272 // Convert libkb.NoSigChainError into one we can report. (See 273 // KBFS-1252). 274 if _, ok := err.(libkb.NoSigChainError); ok { 275 return idutil.NoSigChainError{User: name} 276 } 277 return err 278 } 279 } 280 // The names of implicit teams can change out from under us, 281 // unlike for regular users, so don't require that they remain the 282 // same. 283 if resultName != name && !isImplicit { 284 return fmt.Errorf("Identify returned name=%s, expected %s", 285 resultName, name) 286 } 287 if resultID != id { 288 return fmt.Errorf("Identify returned uid=%s, expected %s", resultID, id) 289 } 290 return nil 291 } 292 293 // identifyUsers identifies the users in the given maps. 294 func identifyUsers( 295 ctx context.Context, nug idutil.NormalizedUsernameGetter, 296 identifier idutil.Identifier, 297 names map[keybase1.UserOrTeamID]kbname.NormalizedUsername, 298 t tlf.Type, offline keybase1.OfflineAvailability) error { 299 eg, ctx := errgroup.WithContext(ctx) 300 301 // TODO: limit the number of concurrent identifies? 302 // TODO: implement a version of errgroup with limited concurrency. 303 for id, name := range names { 304 // Capture range variables. 305 id, name := id, name 306 eg.Go(func() error { 307 return identifyUser(ctx, nug, identifier, name, id, t, offline) 308 }) 309 } 310 311 return eg.Wait() 312 } 313 314 // IdentifyUserList identifies the users in the given list. Only use 315 // this when the usernames are not known - like when rekeying. 316 func IdentifyUserList(ctx context.Context, nug idutil.NormalizedUsernameGetter, 317 identifier idutil.Identifier, ids []keybase1.UserOrTeamID, t tlf.Type, 318 offline keybase1.OfflineAvailability) error { 319 eg, ctx := errgroup.WithContext(ctx) 320 321 // TODO: limit the number of concurrent identifies? 322 // TODO: implement concurrency limited version of errgroup. 323 for _, id := range ids { 324 // Capture range variable. 325 id := id 326 eg.Go(func() error { 327 return identifyUID( 328 ctx, nug, identifier, id, t, offline) 329 }) 330 } 331 332 return eg.Wait() 333 } 334 335 // identifyUsersForTLF is a helper for identifyHandle for easier testing. 336 func identifyUsersForTLF( 337 ctx context.Context, nug idutil.NormalizedUsernameGetter, 338 identifier idutil.Identifier, 339 names map[keybase1.UserOrTeamID]kbname.NormalizedUsername, 340 t tlf.Type, offline keybase1.OfflineAvailability) error { 341 ei := GetExtendedIdentify(ctx) 342 if ei.Behavior == keybase1.TLFIdentifyBehavior_CHAT_SKIP { 343 return nil 344 } 345 346 eg, ctx := errgroup.WithContext(ctx) 347 348 eg.Go(func() error { 349 return ei.makeTlfBreaksIfNeeded(ctx, len(names)) 350 }) 351 352 eg.Go(func() error { 353 return identifyUsers(ctx, nug, identifier, names, t, offline) 354 }) 355 356 return eg.Wait() 357 } 358 359 // IdentifyHandle identifies the canonical names in the given handle. 360 func IdentifyHandle( 361 ctx context.Context, nug idutil.NormalizedUsernameGetter, 362 identifier idutil.Identifier, osg idutil.OfflineStatusGetter, 363 h *Handle) error { 364 offline := keybase1.OfflineAvailability_NONE 365 if osg != nil { 366 offline = osg.OfflineAvailabilityForID(h.tlfID) 367 } 368 return identifyUsersForTLF( 369 ctx, nug, identifier, h.ResolvedUsersMap(), h.Type(), offline) 370 } 371 372 // IdentifySingleAssertion identifies a single assertion, and takes 373 // care of any necessary extended identify behaviors. It does not 374 // relay any broken identify warnings back to the caller, however. 375 func IdentifySingleAssertion( 376 ctx context.Context, assertion, reason string, identifier idutil.Identifier, 377 offline keybase1.OfflineAvailability) ( 378 name kbname.NormalizedUsername, err error) { 379 ei := GetExtendedIdentify(ctx) 380 ctx, cancel := context.WithCancel(ctx) 381 defer cancel() 382 383 // Make sure to wait on any errors. 384 errCh := make(chan error, 1) 385 go func() { 386 errCh <- ei.makeTlfBreaksIfNeeded(ctx, 1) 387 }() 388 389 name, _, err = identifier.Identify(ctx, assertion, reason, offline) 390 if err != nil { 391 return "", err 392 } 393 394 // Wait for the goroutine to finish, but ignore the error. 395 select { 396 case <-errCh: 397 case <-ctx.Done(): 398 return "", ctx.Err() 399 } 400 401 if ei.Behavior.WarningInsteadOfErrorOnBrokenTracks() { 402 _ = GetExtendedIdentify(ctx).GetTlfBreakAndClose() 403 } 404 405 return name, nil 406 }