// approveSub processes a request to initiate an invite or approve a subscription request from another user: // Handle these cases: // A. Manager is inviting another user for the first time (no prior subscription) // B. Manager is re-inviting another user (adjusting modeGiven, modeWant is still "N") // C. Manager is changing modeGiven for another user, modeWant != "N" func (t *Topic) approveSub(h *Hub, sess *Session, target types.Uid, set *MsgClientSet) error { now := time.Now().UTC().Round(time.Millisecond) // Check if requester actually has permission to manage sharing if userData, ok := t.perUser[sess.uid]; !ok || !userData.modeGiven.IsManager() || !userData.modeWant.IsManager() { sess.queueOut(ErrPermissionDenied(set.Id, t.original, now)) return errors.New("topic access denied") } // Parse the access mode granted var modeGiven types.AccessMode if set.Sub.Mode != "" { modeGiven.UnmarshalText([]byte(set.Sub.Mode)) } // If the user is banned from topic, make sute it's the only change if modeGiven.IsBanned() { modeGiven = types.ModeBanned } // Make sure no one but the owner can do an ownership transfer if modeGiven.IsOwner() && t.owner != sess.uid { sess.queueOut(ErrPermissionDenied(set.Id, t.original, now)) return errors.New("attempt to transfer ownership by non-owner") } var givenBefore types.AccessMode // Check if it's a new invite. If so, save it to database as a subscription. // Saved subscription does not mean the user is allowed to post/read userData, ok := t.perUser[target] if !ok { if modeGiven == types.ModeNone { if t.accessAuth != types.ModeNone { // Request to use default access mode for the new subscriptions. modeGiven = t.accessAuth } else { sess.queueOut(ErrMalformed(set.Id, t.original, now)) return errors.New("cannot invite without giving any access rights") } } // Add subscription to database sub := &types.Subscription{ User: target.String(), Topic: t.name, ModeWant: types.ModeNone, ModeGiven: modeGiven, } if err := store.Subs.Create(sub); err != nil { sess.queueOut(ErrUnknown(set.Id, t.original, now)) return err } userData = perUserData{ modeGiven: sub.ModeGiven, modeWant: sub.ModeWant, private: nil, } t.perUser[target] = userData } else { // Action on an existing subscription (re-invite or confirm/decline) givenBefore = userData.modeGiven // Request to re-send invite without changing the access mode if modeGiven == types.ModeNone { modeGiven = userData.modeGiven } else if modeGiven != userData.modeGiven { userData.modeGiven = modeGiven // Save changed value to database if err := store.Subs.Update(t.name, sess.uid, map[string]interface{}{"ModeGiven": modeGiven}); err != nil { return err } t.perUser[target] = userData } } // The user does not want to be bothered, no further action is needed if userData.modeWant.IsBanned() { sess.queueOut(ErrPermissionDenied(set.Id, t.original, now)) return errors.New("topic access denied") } // Handle the following cases: // * a ban of the user, modeGive.IsBanned = true (if user is banned no need to invite anyone) // * regular invite: modeWant = "N", modeGiven > 0 // * access rights update: old modeGiven != new modeGiven if !modeGiven.IsBanned() { if userData.modeWant == types.ModeNone { // (re-)Send the invite to target h.route <- t.makeInvite(target, target, sess.uid, types.InvJoin, userData.modeWant, modeGiven, set.Sub.Info) } else if givenBefore != modeGiven { // Inform target that the access has changed h.route <- t.makeInvite(target, target, sess.uid, types.InvInfo, userData.modeWant, modeGiven, set.Sub.Info) } } // Has anything actually changed? if givenBefore != modeGiven { // inform requester of the change made h.route <- t.makeInvite(sess.uid, target, sess.uid, types.InvInfo, userData.modeWant, modeGiven, map[string]string{"before": givenBefore.String()}) } return nil }
// User requests or updates a self-subscription to a topic // h - hub // sess - originating session // pktId - originating packet Id // want - requested access mode // info - explanation info given by the requester // private - private value to assign to the subscription // Handle these cases: // A. User is trying to subscribe for the first time (no subscription) // B. User is already subscribed, just joining without changing anything // C. User is responsing to an earlier invite (modeWant was "N" in subscription) // D. User is already subscribed, changing modeWant // E. User is accepting ownership transfer (requesting ownership transfer is not permitted) func (t *Topic) requestSub(h *Hub, sess *Session, pktId string, want string, info, private interface{}, loaded bool) error { log.Println("requestSub", t.name) now := time.Now().UTC().Round(time.Millisecond) // Parse the acess mode requested by the user var modeWant types.AccessMode var explicitWant bool if want != "" { log.Println("mode want explicit: ", want) modeWant.UnmarshalText([]byte(want)) explicitWant = true } // If the user wants a self-ban, make sure it's the only change if modeWant.IsBanned() { modeWant = types.ModeBanned } // Vars for saving changes to access mode var updWant *types.AccessMode var updGiven *types.AccessMode // Check if it's an attempt at a new subscription to the topic. If so, save it to database userData, existingSub := t.perUser[sess.uid] if !existingSub { // User requested default access mode. // modeWant could still be ModeNone if the owner wants to manually approve every request if modeWant == types.ModeNone { if explicitWant { // The operation is invalid - user requested to clear access to topic which makes no sense. sess.queueOut(ErrMalformed(pktId, t.original, now)) return errors.New("attempt to clear topic access") } modeWant = t.accessAuth } userData = perUserData{ private: private, modeGiven: t.accessAuth, modeWant: modeWant, } // If it's a re-subscription to a p2p topic, set public if t.cat == types.TopicCat_P2P { // t.perUser contains just one element - other user for uid2, _ := range t.perUser { if user2, err := store.Users.Get(uid2); err != nil { log.Println(err.Error()) sess.queueOut(ErrUnknown(pktId, t.original, now)) return err } else { userData.public = user2.Public } break } } // Add subscription to database sub := &types.Subscription{ User: sess.uid.String(), Topic: t.name, ModeWant: userData.modeWant, ModeGiven: userData.modeGiven, Private: userData.private, } if err := store.Subs.Create(sub); err != nil { log.Println(err.Error()) sess.queueOut(ErrUnknown(pktId, t.original, now)) return err } } else { var ownerChange bool // Process update to existing subscription. It could be an incomplete subscription for a new topic. // If user did not request a new access mode, copy one from cache if !explicitWant && modeWant == types.ModeNone { modeWant = userData.modeWant } if userData.modeGiven.IsOwner() { // Check for possible ownership transfer. Handle the following cases: // 1. Owner joining the topic without any changes // 2. Owner changing own settings // 3. Acceptance or rejection of the ownership transfer // Make sure the current owner cannot unset the owner flag or ban himself if t.owner == sess.uid && (!modeWant.IsOwner() || modeWant.IsBanned()) { log.Println("requestSub: owner attempts to unset the owner flag") sess.queueOut(ErrMalformed(pktId, t.original, now)) return errors.New("cannot unset ownership or ban the owner") } // Ownership transfer ownerChange = modeWant.IsOwner() && !userData.modeWant.IsOwner() // The owner should be able to grant himself any access permissions // If ownership transfer is rejected don't upgrade if modeWant.IsOwner() && !userData.modeGiven.Check(modeWant) { userData.modeGiven |= modeWant updGiven = &userData.modeGiven } } else if modeWant.IsOwner() { // Ownership transfer can only be initiated by the owner sess.queueOut(ErrMalformed(pktId, t.original, now)) return errors.New("non-owner cannot request ownership transfer") } else if userData.modeGiven.IsManager() && modeWant.IsManager() { // The sharer should be able to grant any permissions except ownership if !userData.modeGiven.Check(modeWant & ^types.ModeBanned) { userData.modeGiven |= (modeWant & ^types.ModeBanned) updGiven = &userData.modeGiven } } // Check if t.accessAuth is different now than at the last attempt to subscribe if userData.modeGiven == types.ModeNone && t.accessAuth != types.ModeNone { userData.modeGiven = t.accessAuth updGiven = &userData.modeGiven } // This is a request to change access to whatever is currently available if modeWant == types.ModeNone { modeWant = userData.modeGiven } // Access actually changed if userData.modeWant != modeWant { userData.modeWant = modeWant updWant = &modeWant } // Save changes to DB if updWant != nil || updGiven != nil { update := map[string]interface{}{} // FIXME(gene): gorethink has a bug which causes ModeXYZ to be saved as a string, converting to int if updWant != nil { update["ModeWant"] = int(*updWant) } if updGiven != nil { update["ModeGiven"] = int(*updGiven) } if err := store.Subs.Update(t.name, sess.uid, update); err != nil { sess.queueOut(ErrUnknown(pktId, t.original, now)) return err } //log.Printf("requestSub: topic %s updated SUB: %+#v", t.name, update) } // No transactions in RethinkDB, but two owners are better than none if ownerChange { //log.Printf("requestSub: topic %s owner change", t.name) userData = t.perUser[t.owner] userData.modeGiven = (userData.modeGiven & ^types.ModeOwner) userData.modeWant = (userData.modeWant & ^types.ModeOwner) if err := store.Subs.Update(t.name, t.owner, // FIXME(gene): gorethink has a bug which causes ModeXYZ to be saved as a string, converting to int map[string]interface{}{ "ModeWant": int(userData.modeWant), "ModeGiven": int(userData.modeGiven)}); err != nil { return err } t.perUser[t.owner] = userData t.owner = sess.uid } } t.perUser[sess.uid] = userData // If the user is (self)banned from topic, no further action is needed if modeWant.IsBanned() { t.evictUser(sess.uid, false, sess) // FIXME(gene): need to send a reply to user return errors.New("self-banned access to topic") } else if userData.modeGiven.IsBanned() { sess.queueOut(ErrPermissionDenied(pktId, t.original, now)) return errors.New("topic access denied") } // Don't report existing subscription (no value); // Don't report a newly loaded Grp topic because it's announced later if !existingSub && (t.cat == types.TopicCat_P2P || !loaded) { t.presTopicSubscribed(sess.uid, sess) } else if existingSub { log.Println("pres not published: existing sub") } else { log.Println("pres not published: topic just loaded") } // If requested access mode different from given: if !userData.modeGiven.Check(modeWant) { // Send req to approve to topic managers for uid, pud := range t.perUser { if pud.modeGiven&pud.modeWant&types.ModeShare != 0 { h.route <- t.makeInvite(uid, sess.uid, sess.uid, types.InvAppr, modeWant, userData.modeGiven, info) } } // Send info to requester h.route <- t.makeInvite(sess.uid, sess.uid, sess.uid, types.InvInfo, modeWant, userData.modeGiven, t.public) } return nil }