github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/teams/transactions.go (about) 1 // Copyright 2018 Keybase, Inc. All rights reserved. Use of 2 // this source code is governed by the included BSD license. 3 4 package teams 5 6 import ( 7 "errors" 8 "fmt" 9 10 "golang.org/x/net/context" 11 12 "github.com/keybase/client/go/engine" 13 "github.com/keybase/client/go/externals" 14 "github.com/keybase/client/go/libkb" 15 "github.com/keybase/client/go/protocol/keybase1" 16 "github.com/keybase/client/go/teams/hidden" 17 ) 18 19 // AddMemberTx helps build a transaction that may contain multiple team 20 // sigchain links. The caller can use the transaction to add users to a team 21 // whether they be PUKful or PUKless users, social or server-trust assertions. 22 // 23 // Behind the scenes cryptomembers and invites may be removed if they are for 24 // stale versions of the addees. 25 // 26 // Not thread-safe. 27 type AddMemberTx struct { 28 team *Team 29 payloads []txPayload 30 31 // Error state: if a transaction operation failed and tainted the 32 // transaction, do not allow posting. We try to never get in a state when 33 // we modify a transaction and then realize there's an issue, but if this 34 // happens, make sure `Post` can't be called on such tx later. 35 err error 36 37 // completedInvites is used to mark completed invites, so they are 38 // skipped in sweeping methods. 39 completedInvites map[keybase1.TeamInviteID]bool 40 41 // keep track of how many multiple-use invites have been used so far 42 usedInviteCount map[keybase1.TeamInviteID]int 43 44 // lastChangeForUID holds index of last tx.payloads payload that 45 // affects given uid. 46 lastChangeForUID map[keybase1.UID]int 47 48 // Consumer can set the following to affect AddMemberTx operation: 49 50 // Allow adding users who do not have active Per User Key. Users without 51 // PUK will be added using a 'team.invite' link with type='keybase' 52 // invites. 53 // 54 // If this setting is 'false' (which is the default), it forces AddMemberTx 55 // to never add type='keybase' invites, and only `team.change_membership` 56 // is allowed for adding Keybase users as members. Calls to AddMember* 57 // functions that with a user that does not have a PUK result in an error. 58 AllowPUKless bool 59 60 // Do not return an error when trying to "add a member" who is already 61 // member of the team but has a different role. 62 // 63 // This does not affect team invites (including PUK-less users). For 64 // simplicity, their role can't be changed using AddMemberTx right now. 65 AllowRoleChanges bool 66 67 // Override whether the team key is rotated. 68 SkipKeyRotation *bool 69 70 // EmailInviteMsg is used for sending a welcome message in email invites 71 EmailInviteMsg *string 72 } 73 74 // TransactionTaintedError is used for unrecoverable error where we fail to add 75 // a member and irreversibly break the transaction while doing so. 76 type TransactionTaintedError struct { 77 inner error 78 } 79 80 func (e TransactionTaintedError) Error() string { 81 return fmt.Sprintf("Transaction is in error state: %s", e.inner) 82 } 83 84 // UserPUKlessError is returned when an attempt is made to add a PUKless user 85 // to a transaction that has AllowPUKless=false. 86 type UserPUKlessError struct { 87 username string 88 uv keybase1.UserVersion 89 } 90 91 func (e UserPUKlessError) Error() string { 92 var userStr string 93 if e.username != "" { 94 userStr = fmt.Sprintf("%s (%s)", e.username, e.uv.String()) 95 } else { 96 userStr = e.uv.String() 97 } 98 return fmt.Sprintf("User %s does not have a PUK, cannot be added to this transaction", userStr) 99 } 100 101 type txPayloadTag int 102 103 const ( 104 txPayloadTagCryptomembers txPayloadTag = iota 105 txPayloadTagInviteSocial 106 txPayloadTagInviteKeybase 107 ) 108 109 type txPayload struct { 110 Tag txPayloadTag 111 // txPayload holds either of: *SCTeamInvites or 112 // *keybase1.TeamChangeReq. 113 Val interface{} 114 } 115 116 func CreateAddMemberTx(t *Team) *AddMemberTx { 117 return &AddMemberTx{ 118 team: t, 119 completedInvites: make(map[keybase1.TeamInviteID]bool), 120 lastChangeForUID: make(map[keybase1.UID]int), 121 usedInviteCount: make(map[keybase1.TeamInviteID]int), 122 } 123 } 124 125 func (tx *AddMemberTx) DebugPayloads() (res []interface{}) { 126 for _, v := range tx.payloads { 127 res = append(res, v.Val) 128 } 129 return res 130 } 131 132 func (tx *AddMemberTx) IsEmpty() bool { 133 return len(tx.payloads) == 0 134 } 135 136 // Internal AddMemberTx methods. They should not be used by consumers 137 // of AddMemberTx API. Users of this API should avoid lowercase 138 // methods and fields at all cost, even from same package. 139 140 func (tx *AddMemberTx) findPayload(tag txPayloadTag, forUID keybase1.UID) interface{} { 141 minSeqno := 0 142 hasUID := !forUID.IsNil() 143 if hasUID { 144 minSeqno = tx.lastChangeForUID[forUID] 145 } 146 147 for i, v := range tx.payloads { 148 if i >= minSeqno && v.Tag == tag { 149 if hasUID && i > minSeqno { 150 tx.lastChangeForUID[forUID] = i 151 } 152 return v.Val 153 } 154 } 155 156 if hasUID { 157 tx.lastChangeForUID[forUID] = len(tx.payloads) 158 } 159 ret := txPayload{ 160 Tag: tag, 161 } 162 switch tag { 163 case txPayloadTagCryptomembers: 164 ret.Val = &keybase1.TeamChangeReq{} 165 case txPayloadTagInviteKeybase, txPayloadTagInviteSocial: 166 ret.Val = &SCTeamInvites{} 167 default: 168 panic(fmt.Sprintf("Unexpected tag %q", tag)) 169 } 170 tx.payloads = append(tx.payloads, ret) 171 return ret.Val 172 } 173 174 func (tx *AddMemberTx) inviteKeybasePayload(forUID keybase1.UID) *SCTeamInvites { 175 return tx.findPayload(txPayloadTagInviteKeybase, forUID).(*SCTeamInvites) 176 } 177 178 func (tx *AddMemberTx) inviteSocialPayload(forUID keybase1.UID) *SCTeamInvites { 179 return tx.findPayload(txPayloadTagInviteSocial, forUID).(*SCTeamInvites) 180 } 181 182 func (tx *AddMemberTx) changeMembershipPayload(forUID keybase1.UID) *keybase1.TeamChangeReq { 183 return tx.findPayload(txPayloadTagCryptomembers, forUID).(*keybase1.TeamChangeReq) 184 } 185 186 // Methods modifying payloads are supposed to always succeed given the 187 // preconditions are satisfied. If not, the usual result is either a 188 // no-op or an invalid transaction that is rejected by team player 189 // pre-check or by the server. Public methods should make sure that 190 // internal methods are always called with these preconditions 191 // satisfied. 192 193 func (tx *AddMemberTx) removeMember(uv keybase1.UserVersion) { 194 // Precondition: UV is a cryptomember. 195 payload := tx.changeMembershipPayload(uv.Uid) 196 payload.None = append(payload.None, uv) 197 } 198 199 func (tx *AddMemberTx) addMember(uv keybase1.UserVersion, role keybase1.TeamRole, botSettings *keybase1.TeamBotSettings) error { 200 // Preconditions: UV is a PUKful user, role is valid enum value 201 // and not NONE. 202 payload := tx.changeMembershipPayload(uv.Uid) 203 err := payload.AddUVWithRole(uv, role, botSettings) 204 if err != nil { 205 tx.err = TransactionTaintedError{err} 206 err = tx.err 207 } 208 return err 209 } 210 211 func (tx *AddMemberTx) addMemberAndCompleteInvite(uv keybase1.UserVersion, 212 role keybase1.TeamRole, inviteID keybase1.TeamInviteID) error { 213 // Preconditions: UV is a PUKful user, role is valid and not NONE, invite 214 // exists. Role is not RESTRICTEDBOT as botSettings are set to nil. 215 payload := tx.changeMembershipPayload(uv.Uid) 216 err := payload.AddUVWithRole(uv, role, nil /* botSettings */) 217 if err != nil { 218 tx.err = TransactionTaintedError{err} 219 err = tx.err 220 } 221 payload.CompleteInviteID(inviteID, uv.PercentForm()) 222 return err 223 } 224 225 func appendToInviteList(inv SCTeamInvite, list *[]SCTeamInvite) *[]SCTeamInvite { 226 var tmp []SCTeamInvite 227 if list != nil { 228 tmp = *list 229 } 230 tmp = append(tmp, inv) 231 return &tmp 232 } 233 234 // createKeybaseInvite queues Keybase-type invite for given UV and role. 235 func (tx *AddMemberTx) createKeybaseInvite(uv keybase1.UserVersion, role keybase1.TeamRole) error { 236 // Preconditions: UV is a PUKless user, and not already in the 237 // team, role is valid enum value and not NONE or OWNER. 238 return tx.createInvite("keybase", uv.TeamInviteName(), role, uv.Uid) 239 } 240 241 // createInvite queues an invite for invite name with role. 242 func (tx *AddMemberTx) createInvite(typ string, name keybase1.TeamInviteName, role keybase1.TeamRole, uid keybase1.UID) error { 243 var payload *SCTeamInvites 244 if typ == "keybase" { 245 payload = tx.inviteKeybasePayload(uid) 246 } else { 247 payload = tx.inviteSocialPayload(uid) 248 } 249 250 invite := SCTeamInvite{ 251 Type: typ, 252 Name: name, 253 ID: NewInviteID(), 254 } 255 256 switch role { 257 case keybase1.TeamRole_READER: 258 payload.Readers = appendToInviteList(invite, payload.Readers) 259 case keybase1.TeamRole_WRITER: 260 payload.Writers = appendToInviteList(invite, payload.Writers) 261 case keybase1.TeamRole_ADMIN: 262 payload.Admins = appendToInviteList(invite, payload.Admins) 263 case keybase1.TeamRole_OWNER: 264 payload.Owners = appendToInviteList(invite, payload.Owners) 265 default: 266 tx.err = TransactionTaintedError{fmt.Errorf("invalid role for invite %v", role)} 267 return tx.err 268 } 269 return nil 270 } 271 272 // sweepCryptoMembers will queue "removes" for all cryptomembers with given UID. 273 // exceptAdminsRemovingOwners - But don't try to remove owners if we are admin. 274 func (tx *AddMemberTx) sweepCryptoMembers(ctx context.Context, uid keybase1.UID, 275 exceptAdminsRemovingOwners bool) { 276 team := tx.team 277 var myRole keybase1.TeamRole 278 if exceptAdminsRemovingOwners { 279 var err error 280 myRole, err = tx.team.myRole(ctx) 281 if err != nil { 282 myRole = keybase1.TeamRole_NONE 283 } 284 } 285 for chainUv := range team.chain().inner.UserLog { 286 chainRole := team.chain().getUserRole(chainUv) 287 if chainUv.Uid.Equal(uid) && chainRole != keybase1.TeamRole_NONE { 288 if exceptAdminsRemovingOwners && myRole == keybase1.TeamRole_ADMIN && chainRole == keybase1.TeamRole_OWNER { 289 // Skip if we're an admin and they're an owner. 290 continue 291 } 292 tx.removeMember(chainUv) 293 } 294 } 295 } 296 297 // sweepCryptoMembersOlderThan will queue "removes" for all cryptomembers 298 // with same UID as given and EldestSeqno lower than in given UV. 299 func (tx *AddMemberTx) sweepCryptoMembersOlderThan(uv keybase1.UserVersion) { 300 team := tx.team 301 for chainUv := range team.chain().inner.UserLog { 302 if chainUv.Uid.Equal(uv.Uid) && 303 chainUv.EldestSeqno < uv.EldestSeqno && 304 team.chain().getUserRole(chainUv) != keybase1.TeamRole_NONE { 305 tx.removeMember(chainUv) 306 } 307 } 308 } 309 310 // sweepKeybaseInvites will queue "cancels" for all keybase-type 311 // invites (PUKless members) for given UID. 312 func (tx *AddMemberTx) sweepKeybaseInvites(uid keybase1.UID) { 313 team := tx.team 314 allInvites := team.GetActiveAndObsoleteInvites() 315 for _, invite := range allInvites { 316 if inviteUv, err := invite.KeybaseUserVersion(); err == nil { 317 if inviteUv.Uid.Equal(uid) && !tx.completedInvites[invite.Id] { 318 tx.CancelInvite(invite.Id, uid) 319 } 320 } 321 } 322 } 323 324 func (tx *AddMemberTx) findChangeReqForUV(uv keybase1.UserVersion) *keybase1.TeamChangeReq { 325 for _, v := range tx.payloads { 326 if v.Tag == txPayloadTagCryptomembers { 327 req := v.Val.(*keybase1.TeamChangeReq) 328 for _, x := range req.GetAllAdds() { 329 if x.Eq(uv) { 330 return req 331 } 332 } 333 } 334 } 335 336 return nil 337 } 338 339 // addMemberByUPKV2 is an internal method to add user once we have current 340 // incarnation of UPAK. Public APIs are AddMemberByUV and AddMemberByUsername 341 // that load UPAK and pass it to this function to continue membership changes. 342 // 343 // Return value `invite` is true if user was PUK-less and was added as 344 // keybase-type invite. 345 func (tx *AddMemberTx) addMemberByUPKV2(ctx context.Context, user keybase1.UserPlusKeysV2, role keybase1.TeamRole, 346 botSettings *keybase1.TeamBotSettings) (invite bool, err error) { 347 team := tx.team 348 g := team.G() 349 350 uv := NewUserVersion(user.Uid, user.EldestSeqno) 351 defer g.CTrace(ctx, fmt.Sprintf("AddMemberTx.addMemberByUPKV2(name:%q uv:%v, %v) to team: %q", 352 user.Username, uv, role, team.Name()), &err)() 353 354 if user.Status == keybase1.StatusCode_SCDeleted { 355 return false, libkb.UserDeletedError{Msg: fmt.Sprintf("User %q (%s) is deleted", user.Username, uv.Uid)} 356 } 357 358 if role == keybase1.TeamRole_OWNER && team.IsSubteam() { 359 return false, NewSubteamOwnersError() 360 } 361 362 if err := assertValidNewTeamMemberRole(role); err != nil { 363 return false, err 364 } 365 366 hasPUK := len(user.PerUserKeys) > 0 367 if !hasPUK { 368 g.Log.CDebugf(ctx, "Invite required for %v", uv) 369 370 if !tx.AllowPUKless { 371 return false, UserPUKlessError{username: user.Username, uv: uv} 372 } 373 } 374 375 normalizedUsername := libkb.NewNormalizedUsername(user.Username) 376 377 currentRole, err := team.MemberRole(ctx, uv) 378 if err != nil { 379 return false, err 380 } 381 382 if currentRole != keybase1.TeamRole_NONE { 383 if !hasPUK { 384 return false, fmt.Errorf("user %s (uv %s) is already a member of %s, yet they don't have a PUK", 385 normalizedUsername, uv, team.Name()) 386 } 387 if tx.AllowRoleChanges { 388 if currentRole == role { 389 // No-op team.change_membership links that don't change 390 // member's role are legal, but we are trying to avoid 391 // them. Caller can catch this error and move onwards, 392 // it doesn't taint the transaction. 393 return false, libkb.ExistsError{Msg: fmt.Sprintf("user %s is already a member of team %s with role %s", 394 normalizedUsername, team.Name(), role.HumanString())} 395 } 396 } else { 397 return false, libkb.ExistsError{Msg: fmt.Sprintf("user %s is already a member of team %s", 398 normalizedUsername, team.Name())} 399 } 400 } 401 402 if existingUV, err := team.UserVersionByUID(ctx, uv.Uid); err == nil { 403 // There is an edge case where user is in the middle of resetting 404 // (after reset, before provisioning) and has EldestSeqno=0. 405 if hasPUK && existingUV.EldestSeqno > uv.EldestSeqno { 406 return false, fmt.Errorf("newer version of user %s (uid:%s) already exists in the team %q (%v > %v)", 407 normalizedUsername, uv.Uid, team.Name(), existingUV.EldestSeqno, uv.EldestSeqno) 408 } 409 } 410 411 curInvite, err := team.chain().FindActiveInvite(uv.TeamInviteName(), keybase1.NewTeamInviteTypeDefault(keybase1.TeamInviteCategory_KEYBASE)) 412 if err != nil { 413 if _, ok := err.(libkb.NotFoundError); !ok { 414 return false, err 415 } 416 curInvite = nil 417 err = nil 418 } 419 if curInvite != nil && !hasPUK { 420 return false, libkb.ExistsError{Msg: fmt.Sprintf("user %s is already invited to team %q", 421 normalizedUsername, team.Name())} 422 } 423 424 // No going back after this point! 425 426 tx.sweepKeybaseInvites(uv.Uid) 427 428 if !hasPUK { 429 // An admin is only allowed to remove an owner UV when, in the same 430 // link, replacing them with a 'newer' UV with a greater eldest seqno. 431 // So, if we're an admin re-adding an owner who does not yet have a PUK 432 // then don't try to remove the owner's pre-reset UV. Note that the old 433 // owner UV will still be removed in the transaction during SBS 434 // resolution when they get a PUK later. 435 tx.sweepCryptoMembers(ctx, uv.Uid, true /* exceptAdminsRemovingOwners */) 436 } else { 437 // This might be a role change, only sweep UVs with EldestSeqno older 438 // than one currently being added, so it doesn't sweep the same UV we 439 // are currently adding. 440 tx.sweepCryptoMembersOlderThan(uv) 441 } 442 443 if !hasPUK { 444 if err = tx.createKeybaseInvite(uv, role); err != nil { 445 return false, err 446 } 447 return true /* invite */, nil 448 } 449 if err := tx.addMember(uv, role, botSettings); err != nil { 450 return false, err 451 } 452 return false, nil 453 } 454 455 func (tx *AddMemberTx) completeAllKeybaseInvitesForUID(uv keybase1.UserVersion) error { 456 // Find the right payload first 457 payload := tx.findChangeReqForUV(uv) 458 if payload == nil { 459 return fmt.Errorf("could not find uv %v in transaction", uv) 460 } 461 462 team := tx.team 463 for _, inviteMD := range team.chain().ActiveInvites() { 464 invite := inviteMD.Invite 465 if inviteUv, err := invite.KeybaseUserVersion(); err == nil { 466 if inviteUv.Uid.Equal(uv.Uid) { 467 if payload.CompletedInvites == nil { 468 payload.CompletedInvites = make(map[keybase1.TeamInviteID]keybase1.UserVersionPercentForm) 469 } 470 payload.CompletedInvites[invite.Id] = uv.PercentForm() 471 } 472 } 473 } 474 475 return nil 476 } 477 478 func assertValidNewTeamMemberRole(role keybase1.TeamRole) error { 479 switch role { 480 case keybase1.TeamRole_RESTRICTEDBOT, 481 keybase1.TeamRole_BOT, 482 keybase1.TeamRole_READER, 483 keybase1.TeamRole_WRITER, 484 keybase1.TeamRole_ADMIN, 485 keybase1.TeamRole_OWNER: 486 return nil 487 default: 488 return fmt.Errorf("Unexpected role: %v (%d)", role, int(role)) 489 } 490 } 491 492 // AddMemberByUV will add member by UV and role. It checks if given UV is valid 493 // (that we don't have outdated EldestSeqno), and if user has PUK, and if not, 494 // it properly handles that by adding Keybase-type invite. It also cleans up 495 // old invites and memberships. 496 func (tx *AddMemberTx) AddMemberByUV(ctx context.Context, uv keybase1.UserVersion, role keybase1.TeamRole, 497 botSettings *keybase1.TeamBotSettings) (err error) { 498 team := tx.team 499 g := team.G() 500 501 defer g.CTrace(ctx, fmt.Sprintf("AddMemberTx.AddMemberByUV(%v,%v) to team %q", uv, role, team.Name()), &err)() 502 upak, err := loadUPAK2(ctx, g, uv.Uid, true /* forcePoll */) 503 if err != nil { 504 return err 505 } 506 507 current := upak.Current 508 if uv.EldestSeqno != current.EldestSeqno { 509 return fmt.Errorf("Bad eldestseqno for %s: expected %d, got %d", uv.Uid, current.EldestSeqno, uv.EldestSeqno) 510 } 511 512 _, err = tx.addMemberByUPKV2(ctx, current, role, botSettings) 513 return err 514 } 515 516 // AddMemberByUsername will add member by username and role. It checks if given 517 // username can become crypto member or a PUKless member. It will also clean up 518 // old invites and memberships if necessary. 519 func (tx *AddMemberTx) AddMemberByUsername(ctx context.Context, username string, role keybase1.TeamRole, 520 botSettings *keybase1.TeamBotSettings) (err error) { 521 team := tx.team 522 mctx := team.MetaContext(ctx) 523 524 defer mctx.Trace(fmt.Sprintf("AddMemberTx.AddMemberByUsername(%s,%v) to team %q", username, role, team.Name()), &err)() 525 526 upak, err := engine.ResolveAndCheck(mctx, username, true /* useTracking */) 527 if err != nil { 528 return err 529 } 530 _, err = tx.addMemberByUPKV2(ctx, upak, role, botSettings) 531 return err 532 } 533 534 // preprocessAssertion takes an input assertion and determines if this is a 535 // valid Keybase-style assertion. If it's an email (or phone) assertion, we 536 // assert that it only has one part (and isn't a+b compound). If there is only 537 // one factor in the assertion, then that's returned as single. Otherwise, 538 // single is nil. 539 func preprocessAssertion(mctx libkb.MetaContext, assertionStr string) (isServerTrustInvite bool, single libkb.AssertionURL, full libkb.AssertionExpression, err error) { 540 assertion, err := externals.AssertionParseAndOnly(mctx, assertionStr) 541 if err != nil { 542 return false, nil, nil, err 543 } 544 urls := assertion.CollectUrls(nil) 545 if len(urls) == 1 { 546 single = urls[0] 547 } 548 for _, u := range urls { 549 if u.IsServerTrust() { 550 isServerTrustInvite = true 551 } 552 } 553 if isServerTrustInvite && len(urls) > 1 { 554 return false, nil, nil, NewMixedServerTrustAssertionError() 555 } 556 return isServerTrustInvite, single, assertion, nil 557 } 558 559 // resolveServerTrustAssertion resolves server-trust assertion. The possible 560 // outcomes are: 561 // 1) assertion resolves to a user: userFound=true, non-empty upak. 562 // 2) assertion did not resolve to a user: userFound=false, empty upak. 563 // 3) we got a server error when resolving, abort. 564 func resolveServerTrustAssertion(mctx libkb.MetaContext, assertion string) (upak keybase1.UserPlusKeysV2, userFound bool, err error) { 565 user, resolveRes, err := mctx.G().Resolver.ResolveUser(mctx, assertion) 566 if err != nil { 567 if shouldPreventTeamCreation(err) { 568 // Resolution failed because of server error, do not try to invite 569 // because it might be a resolvable assertion. We don't know if 570 // that assertion resolves to a user. Should abort anything we are 571 // trying to do. 572 return upak, false, err 573 } 574 // Assertion did not resolve, but we didn't get error preventing us 575 // from inviting either. Invite assertion to the team. 576 return upak, false, nil 577 } 578 579 if !resolveRes.IsServerTrust() { 580 return upak, false, fmt.Errorf("Unexpected non server-trust resolution returned: %q", assertion) 581 } 582 upak, err = engine.ResolveAndCheck(mctx, user.Username, true /* useTracking */) 583 if err != nil { 584 return upak, false, err 585 } 586 // Success - assertion server-trust resolves to upak, we can just add them 587 // as a member. 588 return upak, true, nil 589 } 590 591 // AddMemberCandidate is created by ResolveUPKV2FromAssertion and should be 592 // passed to AddOrInviteMemberCandidate. 593 type AddMemberCandidate struct { 594 SourceAssertion string 595 596 // Assertion parsing results: 597 Full libkb.AssertionExpression // always set 598 Single libkb.AssertionURL // not nil if assertion was a single (not compound) 599 600 // If resolved to a Keybase user, KeybaseUser is non-nil. 601 KeybaseUser *keybase1.UserPlusKeysV2 602 603 // Resolved user might be PUKless, so adding them might still result in an 604 // invite (type=keybase). If KeybaseUser is nil, adding that candidate 605 // shall result in social invite for Single assertion, provided by source 606 // assertion was not compound. If it was, that person can't be added. 607 } 608 609 func (a AddMemberCandidate) DebugString() string { 610 if a.KeybaseUser != nil { 611 return fmt.Sprintf("User=%q, IsSingle=%t", a.KeybaseUser.Username, a.Single != nil) 612 } 613 return fmt.Sprintf("User=nil, IsSingle=%t", a.Single != nil) 614 } 615 616 // ResolveUPKV2FromAssertion creates an AddMemberCandidate struct by parsing 617 // and attempting to resolve an assertion. This result can be then passed to 618 // AddOrInviteMemberCandidate to queue adding that person in the transaction. 619 // ResolveUPKV2FromAssertion itself does not modify the transaction. 620 // 621 // If your use case is: 622 // - you have an assertion, 623 // - that should be resolved, 624 // - and based on the resolution it should either add it to the transaction or 625 // not, 626 // 627 // this is the way to go. 628 // 629 // See documentation of AddOrInviteMemberByAssertion to find out what assertion 630 // types are supported. 631 // 632 // AddOrInviteMemberByAssertion does the same thing internally, but you don't 633 // get to check resolution result until after transaction is modified. 634 func (tx *AddMemberTx) ResolveUPKV2FromAssertion(m libkb.MetaContext, assertion string) (ret AddMemberCandidate, err error) { 635 isServerTrustInvite, single, full, err := preprocessAssertion(m, assertion) 636 if err != nil { 637 return ret, err 638 } 639 640 ret.SourceAssertion = assertion 641 ret.Full = full 642 ret.Single = single 643 644 if isServerTrustInvite { 645 // Server-trust assertions (`phone`/`email`): ask server if it resolves 646 // to a user. 647 upak, userFound, err := resolveServerTrustAssertion(m, assertion) 648 if err != nil { 649 return ret, err 650 } 651 if userFound { 652 ret.KeybaseUser = &upak 653 } 654 } else { 655 // Normal assertion: resolve and verify. 656 upak, err := engine.ResolveAndCheck(m, assertion, true /* useTracking */) 657 if err != nil { 658 if rErr, ok := err.(libkb.ResolutionError); !ok || (rErr.Kind != libkb.ResolutionErrorNotFound) { 659 return ret, err 660 } 661 // Resolution not found - fall through with nil KeybaseUser. 662 } else { 663 ret.KeybaseUser = &upak 664 } 665 } 666 667 return ret, nil 668 } 669 670 // AddOrInviteMemberCandidate adds a member using AddMemberCandidate struct 671 // that can be obtained by calling ResolveUPKV2FromAssertion with assertion 672 // string. 673 func (tx *AddMemberTx) AddOrInviteMemberCandidate(ctx context.Context, candidate AddMemberCandidate, role keybase1.TeamRole, botSettings *keybase1.TeamBotSettings) ( 674 username libkb.NormalizedUsername, uv keybase1.UserVersion, invite bool, err error) { 675 team := tx.team 676 mctx := team.MetaContext(ctx) 677 678 defer mctx.Trace(fmt.Sprintf("AddMemberTx.AddOrInviteMemberCandidate(%q,keybaseUser=%t,%v) to team %q", 679 candidate.SourceAssertion, candidate.KeybaseUser != nil, role, team.Name()), &err)() 680 681 if candidate.KeybaseUser != nil { 682 // We have a user and can add them to a team, no invite required. 683 username = libkb.NewNormalizedUsername(candidate.KeybaseUser.Username) 684 uv = candidate.KeybaseUser.ToUserVersion() 685 invite, err = tx.addMemberByUPKV2(ctx, *candidate.KeybaseUser, role, botSettings) 686 mctx.Debug("Adding Keybase user: %s (isInvite=%v)", username, invite) 687 return username, uv, invite, err 688 } 689 690 // We are on invite path here. 691 692 if candidate.Single == nil { 693 // Compound assertions are invalid at this point. 694 return "", uv, false, NewCompoundInviteError(candidate.SourceAssertion) 695 } 696 697 typ, name := candidate.Single.ToKeyValuePair() 698 mctx.Debug("team %s invite sbs member %s/%s", team.Name(), typ, name) 699 700 // Sanity checks: 701 // Can't do SBS invite with role=OWNER. 702 if role.IsOrAbove(keybase1.TeamRole_OWNER) { 703 return "", uv, false, NewAttemptedInviteSocialOwnerError(candidate.SourceAssertion) 704 } 705 inviteName := keybase1.TeamInviteName(name) 706 // Can't invite if invite for same SBS assertion already exists in that 707 // team. 708 existing, err := tx.team.HasActiveInvite(mctx, inviteName, typ) 709 if err != nil { 710 return "", uv, false, err 711 } 712 if existing { 713 return "", uv, false, libkb.ExistsError{ 714 Msg: fmt.Sprintf("Invite for %q already exists", candidate.Single.String()), 715 } 716 } 717 718 // All good - add invite to tx. 719 if err = tx.createInvite(typ, inviteName, role, "" /* uid */); err != nil { 720 return "", uv, false, err 721 } 722 return "", uv, true, nil 723 724 } 725 726 // AddOrInviteMemberByAssertion adds an assertion to the team. It can 727 // handle three major cases: 728 // 729 // 1. joe OR joe+foo@reddit WHERE joe is already a keybase user, or the 730 // assertions map to a unique Keybase user 731 // 2. joe@reddit WHERE joe isn't a keybase user, and this is a social 732 // invitations 733 // 3. [bob@gmail.com]@email WHERE server-trust resolution is performed and 734 // either TOFU invite is created or resolved member is added. Same works 735 // with `@phone`. 736 // 737 // **Does** attempt to resolve the assertion, to distinguish between case (1), 738 // case (2) and an error The return values (uv, username) can both be 739 // zero-valued if the assertion is not a Keybase user. 740 func (tx *AddMemberTx) AddOrInviteMemberByAssertion(ctx context.Context, assertion string, role keybase1.TeamRole, botSettings *keybase1.TeamBotSettings) ( 741 username libkb.NormalizedUsername, uv keybase1.UserVersion, invite bool, err error) { 742 team := tx.team 743 m := team.MetaContext(ctx) 744 745 defer m.Trace(fmt.Sprintf("AddMemberTx.AddOrInviteMemberByAssertion(%s,%v) to team %q", assertion, role, team.Name()), &err)() 746 747 candidate, err := tx.ResolveUPKV2FromAssertion(m, assertion) 748 if err != nil { 749 return "", uv, false, err 750 } 751 return tx.AddOrInviteMemberCandidate(ctx, candidate, role, botSettings) 752 } 753 754 // CanConsumeInvite checks if invite can be used. Has to be called before 755 // calling `ConsumeInviteByID` with that invite ID. Does not modify the 756 // transaction. When handling team invites, it should be called before 757 // `ConsumeInviteByID` to assert that invite is still usable (new-style invites 758 // may be expired or exceeded). 759 func (tx *AddMemberTx) CanConsumeInvite(ctx context.Context, inviteID keybase1.TeamInviteID) error { 760 inviteMD, found := tx.team.chain().FindActiveInviteMDByID(inviteID) 761 if !found { 762 return fmt.Errorf("failed to find invite being used") 763 } 764 invite := inviteMD.Invite 765 766 isNewStyle, err := IsNewStyleInvite(invite) 767 if err != nil { 768 return err 769 } 770 771 if isNewStyle { 772 // Only need to check new-style invites. Old-style invites cannot have 773 // expiration date and can always be one-time use, and wouldn't show up 774 // in `FindActiveInviteByID` (because they are not active). New-style 775 // invites always stay active. 776 alreadyUsedBeforeTransaction := len(inviteMD.UsedInvites) 777 alreadyUsed := alreadyUsedBeforeTransaction + tx.usedInviteCount[inviteID] 778 if invite.IsUsedUp(alreadyUsed) { 779 return NewInviteLinkAcceptanceError("invite has no more uses left; so cannot add by this invite") 780 } 781 782 now := tx.team.G().Clock().Now() 783 if invite.IsExpired(now) { 784 return NewInviteLinkAcceptanceError("invite expired at %v which is before the current time of %v; rejecting", invite.Etime, now) 785 } 786 } else { 787 _, alreadyCompleted := tx.completedInvites[inviteID] 788 if alreadyCompleted { 789 return fmt.Errorf("invite ID %s was already completed in this transaction", inviteID) 790 } 791 } 792 793 return nil 794 } 795 796 // ConsumeInviteByID finds a change membership payload and either sets 797 // "completed invite" or adds am "used invite". `CanConsumeInvite` has to be 798 // called before this function. 799 func (tx *AddMemberTx) ConsumeInviteByID(ctx context.Context, inviteID keybase1.TeamInviteID, uv keybase1.UserVersion) error { 800 payload := tx.findChangeReqForUV(uv) 801 if payload == nil { 802 return fmt.Errorf("could not find uv %v in transaction", uv) 803 } 804 805 inviteMD, found := tx.team.chain().FindActiveInviteMDByID(inviteID) 806 if !found { 807 return fmt.Errorf("failed to find invite being used") 808 } 809 invite := inviteMD.Invite 810 811 isNewStyle, err := IsNewStyleInvite(invite) 812 if err != nil { 813 return err 814 } 815 816 if isNewStyle { 817 payload.UseInviteID(inviteID, uv.PercentForm()) 818 tx.usedInviteCount[inviteID]++ 819 } else { 820 payload.CompleteInviteID(inviteID, uv.PercentForm()) 821 tx.completedInvites[inviteID] = true 822 } 823 824 return nil 825 } 826 827 // CompleteSocialInvitesFor finds all proofs for `uv` and tries to match them 828 // with active social invites in the team. Any invite that matches the proofs 829 // and can therefore be "completed" by adding `uv` to the team is marked as 830 // completed. 831 // 832 // The purpose of this function is to complete social invites when user is 833 // being added outside of SBS handling. There are two cases in which this 834 // function completes an invite: 835 // 836 // 1. An admin is racing SBS handler by adding a user after they add a proof but 837 // before server sends out SBS notifications. 838 // 2. There was more than one social invite that resolved to the same user 839 // 3. ...or other cases (or bugs) when there are outstanding invites that 840 // resolve to a user but they were not added through SBS handler. 841 // 842 // Note that (2) is likely still not handled correctly if there are social 843 // invites that someone who is already in the team adds proofs for. 844 func (tx *AddMemberTx) CompleteSocialInvitesFor(ctx context.Context, uv keybase1.UserVersion, username string) (err error) { 845 team := tx.team 846 g := team.G() 847 848 defer g.CTrace(ctx, fmt.Sprintf("AddMemberTx.CompleteSocialInvitesFor(%v,%s)", uv, username), &err)() 849 if team.NumActiveInvites() == 0 { 850 g.Log.CDebugf(ctx, "CompleteSocialInvitesFor: no active invites in team") 851 return nil 852 } 853 854 // Find the right payload first 855 payload := tx.findChangeReqForUV(uv) 856 if payload == nil { 857 return fmt.Errorf("could not find uv %v in transaction", uv) 858 } 859 860 proofs, identifyOutcome, err := getUserProofsNoTracking(ctx, g, username) 861 if err != nil { 862 return err 863 } 864 865 var completedInvites = map[keybase1.TeamInviteID]keybase1.UserVersionPercentForm{} 866 867 for _, inviteMD := range team.chain().ActiveInvites() { 868 invite := inviteMD.Invite 869 ityp, err := invite.Type.String() 870 if err != nil { 871 return err 872 } 873 category, err := invite.Type.C() 874 if err != nil { 875 return err 876 } 877 878 if category != keybase1.TeamInviteCategory_SBS { 879 continue 880 } 881 882 proofsWithType := proofs.Get([]string{ityp}) 883 884 var proof *libkb.Proof 885 for _, p := range proofsWithType { 886 if p.Value == string(invite.Name) { 887 proof = &p 888 break 889 } 890 } 891 892 if proof == nil { 893 continue 894 } 895 896 g.Log.CDebugf(ctx, "CompleteSocialInvitesFor: Found proof in user's ProofSet: key: %s value: %q", proof.Key, proof.Value) 897 proofErr := identifyOutcome.GetRemoteCheckResultFor(ityp, string(invite.Name)) 898 g.Log.CDebugf(ctx, "CompleteSocialInvitesFor: proof result -> %v", proofErr) 899 if proofErr == nil { 900 completedInvites[invite.Id] = uv.PercentForm() 901 g.Log.CDebugf(ctx, "CompleteSocialInvitesFor: Found completed invite: %s -> %v", invite.Id, uv) 902 } 903 } 904 905 // After checking everything, mutate payload. 906 g.Log.CDebugf(ctx, "CompleteSocialInvitesFor: checked invites without errors, adding %d complete(s)", len(completedInvites)) 907 if len(completedInvites) > 0 { 908 if payload.CompletedInvites == nil { 909 payload.CompletedInvites = make(map[keybase1.TeamInviteID]keybase1.UserVersionPercentForm) 910 } 911 for i, v := range completedInvites { 912 payload.CompletedInvites[i] = v 913 } 914 } 915 916 return nil 917 } 918 919 func (tx *AddMemberTx) ReAddMemberToImplicitTeam(ctx context.Context, uv keybase1.UserVersion, hasPUK bool, role keybase1.TeamRole, 920 botSettings *keybase1.TeamBotSettings) error { 921 if !tx.team.IsImplicit() { 922 return fmt.Errorf("ReAddMemberToImplicitTeam only works on implicit teams") 923 } 924 if err := assertValidNewTeamMemberRole(role); err != nil { 925 return err 926 } 927 928 if hasPUK { 929 if err := tx.addMember(uv, role, botSettings); err != nil { 930 return err 931 } 932 tx.sweepCryptoMembers(ctx, uv.Uid, false) 933 if err := tx.completeAllKeybaseInvitesForUID(uv); err != nil { 934 return err 935 } 936 } else { 937 if !tx.AllowPUKless { 938 return UserPUKlessError{uv: uv} 939 } 940 if err := tx.createKeybaseInvite(uv, role); err != nil { 941 return err 942 } 943 tx.sweepKeybaseInvites(uv.Uid) 944 // We cannot sweep crypto members here because we need to ensure that 945 // we are only posting one link, and if we want to add a pukless 946 // member, it has to be invite link. Otherwise we would attempt to 947 // remove the old member without adding a new one. So old crypto 948 // members have to stay for now. However, old crypto member should be 949 // swept when Keybase-type invite goes through SBS handler and invited 950 // member becomes a real crypto dude. 951 } 952 953 if len(tx.payloads) != 1 { 954 return errors.New("ReAddMemberToImplicitTeam tried to create more than one link") 955 } 956 957 return nil 958 } 959 960 func (tx *AddMemberTx) CancelInvite(id keybase1.TeamInviteID, forUID keybase1.UID) { 961 payload := tx.inviteKeybasePayload(forUID) 962 if payload.Cancel == nil { 963 payload.Cancel = &[]SCTeamInviteID{SCTeamInviteID(id)} 964 } else { 965 tmp := append(*payload.Cancel, SCTeamInviteID(id)) 966 payload.Cancel = &tmp 967 } 968 } 969 970 // AddMemberBySBS is very similar in what it does to addMemberByUPKV2 971 // (or AddMemberBy* family of functions), but it has easier job 972 // because it only adds cryptomembers and fails on PUKless users. It 973 // also sets invite referenced by `invitee.InviteID` as Completed by 974 // UserVersion from `invitee` in the same ChangeMembership link that 975 // adds the user to the team. 976 // 977 // AddMemberBySBS assumes that member role is already checked by the 978 // caller, so it might generate invalid signature if invitee is 979 // already a member with same role. 980 func (tx *AddMemberTx) AddMemberBySBS(ctx context.Context, invitee keybase1.TeamInvitee, role keybase1.TeamRole) (err error) { 981 team := tx.team 982 g := team.G() 983 984 defer g.CTrace(ctx, fmt.Sprintf("AddMemberTx.AddMemberBySBS(%v) to team: %q", 985 invitee, team.Name()), &err)() 986 987 uv := NewUserVersion(invitee.Uid, invitee.EldestSeqno) 988 upak, err := loadUPAK2(ctx, g, uv.Uid, true /* forcePoll */) 989 if err != nil { 990 return err 991 } 992 993 user := upak.Current 994 if uv.EldestSeqno != user.EldestSeqno { 995 return fmt.Errorf("Bad eldestseqno for %s: expected %d, got %d", uv.Uid, user.EldestSeqno, uv.EldestSeqno) 996 } 997 998 if len(user.PerUserKeys) == 0 { 999 return fmt.Errorf("Cannot add PUKless user %q (%s) for SBS", user.Username, uv.Uid) 1000 } 1001 1002 if user.Status == keybase1.StatusCode_SCDeleted { 1003 return fmt.Errorf("User %q (%s) is deleted", user.Username, uv.Uid) 1004 } 1005 1006 if role == keybase1.TeamRole_OWNER && team.IsSubteam() { 1007 return NewSubteamOwnersError() 1008 } 1009 1010 if err := assertValidNewTeamMemberRole(role); err != nil { 1011 return err 1012 } 1013 1014 // Mark that we will be completing inviteID so sweepKeybaseInvites 1015 // does not cancel it if it happens to be keybase-type. 1016 tx.completedInvites[invitee.InviteID] = true 1017 1018 tx.sweepKeybaseInvites(uv.Uid) 1019 tx.sweepCryptoMembersOlderThan(uv) 1020 if err := tx.addMemberAndCompleteInvite(uv, role, invitee.InviteID); err != nil { 1021 return err 1022 } 1023 return nil 1024 } 1025 1026 func (tx *AddMemberTx) Post(mctx libkb.MetaContext) (err error) { 1027 team := tx.team 1028 g := team.G() 1029 1030 defer g.CTrace(mctx.Ctx(), "AddMemberTx.Post", &err)() 1031 1032 if tx.err != nil { 1033 // AddMemberTx operation has irreversibly failed, potentially leaving 1034 // tx in bad state. Do not post. 1035 return tx.err 1036 } 1037 1038 if len(tx.payloads) == 0 { 1039 return errors.New("there are no signatures to post") 1040 } 1041 1042 g.Log.CDebugf(mctx.Ctx(), "AddMemberTx: Attempting to post %d signatures", len(tx.payloads)) 1043 1044 // Initialize key manager. 1045 if _, err := team.SharedSecret(mctx.Ctx()); err != nil { 1046 return err 1047 } 1048 1049 // Make sure we know recent merkle root. 1050 if err := team.ForceMerkleRootUpdate(mctx.Ctx()); err != nil { 1051 return err 1052 } 1053 1054 // Get admin permission, we will use the same one for all sigs. 1055 admin, err := team.getAdminPermission(mctx.Ctx()) 1056 if err != nil { 1057 return err 1058 } 1059 1060 var sections []SCTeamSection 1061 memSet := newMemberSet() 1062 var sectionsWithBoxSummaries []int 1063 var ratchet *hidden.Ratchet 1064 1065 // Transform payloads to SCTeamSections. 1066 for i, p := range tx.payloads { 1067 section := SCTeamSection{ 1068 ID: SCTeamID(team.ID), 1069 Admin: admin, 1070 Implicit: team.IsImplicit(), 1071 Public: team.IsPublic(), 1072 } 1073 1074 // Only add a ratchet to the first link in the sequence, it doesn't make sense 1075 // to add more than one, and it may as well be the first. 1076 if ratchet == nil { 1077 ratchet, err = team.makeRatchet(mctx.Ctx()) 1078 if err != nil { 1079 return err 1080 } 1081 } 1082 section.Ratchets = ratchet.ToTeamSection() 1083 1084 switch p.Tag { 1085 case txPayloadTagCryptomembers: 1086 payload := p.Val.(*keybase1.TeamChangeReq) 1087 // We need memberSet for this particular payload, but also keep a 1088 // memberSet for entire transaction to generate boxes afterwards. 1089 payloadMemberSet, err := newMemberSetChange(mctx.Ctx(), g, *payload) 1090 if err != nil { 1091 return err 1092 } 1093 1094 memSet.appendMemberSet(payloadMemberSet) 1095 1096 section.Members, err = payloadMemberSet.Section() 1097 if err != nil { 1098 return err 1099 } 1100 1101 section.CompletedInvites = payload.CompletedInvites 1102 section.UsedInvites = makeSCMapInviteIDUVMap(payload.UsedInvites) 1103 1104 sections = append(sections, section) 1105 1106 // If there are additions, then there will be a new key involved. 1107 // If there are deletions, then we'll be rotating. So either way, 1108 // this section needs a box summary. 1109 sectionsWithBoxSummaries = append(sectionsWithBoxSummaries, i) 1110 case txPayloadTagInviteKeybase, txPayloadTagInviteSocial: 1111 entropy, err := makeSCTeamEntropy() 1112 if err != nil { 1113 return err 1114 } 1115 1116 section.Invites = p.Val.(*SCTeamInvites) 1117 if section.Invites.Len() == 0 { 1118 return fmt.Errorf("invalid invite, 0 members invited") 1119 } 1120 section.Entropy = entropy 1121 sections = append(sections, section) 1122 1123 if !tx.AllowPUKless && p.Tag == txPayloadTagInviteKeybase && section.Invites.HasNewInvites() { 1124 // This means we broke contract somewhere or that tx.AllowPUKless 1125 // was changed to false after adding PUKless user. Better fail here 1126 // instead of doing unexpected. 1127 return fmt.Errorf("Found payload with new Keybase invites but AllowPUKless is false") 1128 } 1129 default: 1130 return fmt.Errorf("Unhandled case in AddMemberTx.Post, unknown tag: %v", p.Tag) 1131 } 1132 } 1133 1134 // If memSet has any downgrades, request downgrade lease. 1135 var merkleRoot *libkb.MerkleRoot 1136 var lease *libkb.Lease 1137 1138 downgrades, err := team.getDowngradedUsers(mctx.Ctx(), memSet) 1139 if err != nil { 1140 return err 1141 } 1142 if len(downgrades) != 0 { 1143 lease, merkleRoot, err = libkb.RequestDowngradeLeaseByTeam(mctx.Ctx(), g, team.ID, downgrades) 1144 if err != nil { 1145 return err 1146 } 1147 // Always cancel lease so we don't leave any hanging. 1148 defer func() { 1149 err := libkb.CancelDowngradeLease(mctx.Ctx(), g, lease.LeaseID) 1150 if err != nil { 1151 g.Log.CWarningf(mctx.Ctx(), "Failed to cancel downgrade lease: %s", err.Error()) 1152 } 1153 }() 1154 } 1155 1156 var skipKeyRotation bool 1157 if tx.SkipKeyRotation != nil { 1158 skipKeyRotation = *tx.SkipKeyRotation 1159 } else { 1160 skipKeyRotation = team.CanSkipKeyRotation() 1161 } 1162 secretBoxes, implicitAdminBoxes, perTeamKeySection, teamEKPayload, err := team.recipientBoxes(mctx.Ctx(), memSet, skipKeyRotation) 1163 if err != nil { 1164 return err 1165 } 1166 1167 // For all sections that we previously did add/remove members for, let's 1168 for _, s := range sectionsWithBoxSummaries { 1169 err = addSummaryHash(§ions[s], secretBoxes) 1170 if err != nil { 1171 return err 1172 } 1173 } 1174 1175 if perTeamKeySection != nil { 1176 // We have a new per team key, find first TeamChangeReq 1177 // section that removes users and add it there. 1178 found := false 1179 for i, v := range tx.payloads { 1180 if v.Tag == txPayloadTagCryptomembers { 1181 req := v.Val.(*keybase1.TeamChangeReq) 1182 if len(req.None) > 0 { 1183 sections[i].PerTeamKey = perTeamKeySection 1184 found = true 1185 break 1186 } 1187 } 1188 } 1189 if !found { 1190 return fmt.Errorf("AddMemberTx.Post got a PerTeamKey but couldn't find a link with None to attach it") 1191 } 1192 } 1193 1194 var teamEKBoxes *[]keybase1.TeamEkBoxMetadata 1195 if teamEKPayload == nil { 1196 ekLib := g.GetEKLib() 1197 if ekLib != nil && len(memSet.recipients) > 0 { 1198 uids := memSet.recipientUids() 1199 teamEKBoxes, err = ekLib.BoxLatestTeamEK(mctx, team.ID, uids) 1200 if err != nil { 1201 return err 1202 } 1203 } 1204 } 1205 1206 // Take payloads and team sections and generate chain of signatures. 1207 nextSeqno := team.NextSeqno() 1208 latestLinkID := team.chain().GetLatestLinkID() 1209 1210 var readySigs []libkb.SigMultiItem 1211 for i, section := range sections { 1212 var linkType libkb.LinkType 1213 switch tx.payloads[i].Tag { 1214 case txPayloadTagCryptomembers: 1215 linkType = libkb.LinkTypeChangeMembership 1216 case txPayloadTagInviteKeybase, txPayloadTagInviteSocial: 1217 linkType = libkb.LinkTypeInvite 1218 default: 1219 return fmt.Errorf("Unhandled case in AddMemberTx.Post, unknown tag: %v", tx.payloads[i].Tag) 1220 } 1221 1222 sigMultiItem, linkID, err := team.sigTeamItemRaw(mctx.Ctx(), section, linkType, 1223 nextSeqno, latestLinkID, merkleRoot) 1224 if err != nil { 1225 return err 1226 } 1227 1228 g.Log.CDebugf(mctx.Ctx(), "AddMemberTx: Prepared signature %d: Type: %v SeqNo: %d Hash: %q", 1229 i, linkType, nextSeqno, linkID) 1230 1231 nextSeqno++ 1232 latestLinkID = linkID 1233 readySigs = append(readySigs, sigMultiItem) 1234 } 1235 1236 // Add a single bot_settings link if we are adding any RESTRICTEDBOT members 1237 if len(memSet.restrictedBotSettings) > 0 { 1238 var section SCTeamSection 1239 section, ratchet, err = team.botSettingsSection(mctx.Ctx(), memSet.restrictedBotSettings, ratchet, merkleRoot) 1240 if err != nil { 1241 return err 1242 } 1243 1244 sigMultiItem, linkID, err := team.sigTeamItemRaw(mctx.Ctx(), section, libkb.LinkTypeTeamBotSettings, 1245 nextSeqno, latestLinkID, merkleRoot) 1246 if err != nil { 1247 return err 1248 } 1249 1250 g.Log.CDebugf(mctx.Ctx(), "AddMemberTx: Prepared bot_settings signature: SeqNo: %d Hash: %q", 1251 nextSeqno, linkID) 1252 nextSeqno++ 1253 readySigs = append(readySigs, sigMultiItem) 1254 } 1255 1256 if err := team.precheckLinksToPost(mctx.Ctx(), readySigs); err != nil { 1257 g.Log.CDebugf(mctx.Ctx(), "Precheck failed: %v", err) 1258 return err 1259 } 1260 1261 payloadArgs := sigPayloadArgs{ 1262 secretBoxes: secretBoxes, 1263 lease: lease, 1264 implicitAdminBoxes: implicitAdminBoxes, 1265 teamEKPayload: teamEKPayload, 1266 teamEKBoxes: teamEKBoxes, 1267 ratchetBlindingKeys: ratchet.ToSigPayload(), 1268 } 1269 payload := team.sigPayload(readySigs, payloadArgs) 1270 if tx.EmailInviteMsg != nil { 1271 payload["email_invite_msg"] = *tx.EmailInviteMsg 1272 } 1273 1274 if err := team.postMulti(mctx, payload); err != nil { 1275 return err 1276 } 1277 1278 // nextSeqno-1 should be the sequence number of last link that we sent. 1279 err = team.notify(mctx.Ctx(), keybase1.TeamChangeSet{MembershipChanged: true}, nextSeqno-1) 1280 if err != nil { 1281 mctx.Warning("Failed to send team change notification: %s", err) 1282 } 1283 1284 team.storeTeamEKPayload(mctx.Ctx(), teamEKPayload) 1285 createTeambotKeys(team.G(), team.ID, memSet.restrictedBotRecipientUids()) 1286 1287 return nil 1288 }