| 1 | package sheets |
| 2 | |
| 3 | import ( |
| 4 | "net/http" |
| 5 | "strconv" |
| 6 | ) |
| 7 | |
| 8 | // noticeRevokedTeam is the code for a revoke that removed a grant and |
| 9 | // did not remove anybody's access. |
| 10 | // |
| 11 | // Team standing reaches every sheet on the instance as an editor, by |
| 12 | // design (spec §8.2), and an explicit grant sits on top of it. So |
| 13 | // removing a teammate's grant takes the grant away and leaves the |
| 14 | // person able to open the sheet. The screen used to report the first |
| 15 | // half and stay silent about the second, which let an owner read |
| 16 | // "Access removed", walk away, and believe they had shut somebody out |
| 17 | // of a sealed sheet. |
| 18 | // |
| 19 | // RULED by Paul, 2026-09-01: the sentence is what changes here, not the |
| 20 | // behaviour. Removing a restricting grant from a teammate still widens |
| 21 | // their access from viewer to editor — see the issue filed against this |
| 22 | // commit, which records that as a separate, open question rather than |
| 23 | // letting it disappear behind a notice that now describes it politely. |
| 24 | const noticeRevokedTeam = "revoked-team" |
| 25 | |
| 26 | // revokedTeamText is the sentence when we cannot name the person. |
| 27 | // |
| 28 | // Paul's wording, with the name generalised for the fallback below. |
| 29 | const revokedTeamText = "The explicit permission is removed, but since they are " + |
| 30 | "on the team, they still have access." |
| 31 | |
| 32 | // stillReaches reports whether somebody can still open a sheet. |
| 33 | // |
| 34 | // Asked AFTER the revoke, and asked of roleFor rather than reasoned |
| 35 | // about separately, so the notice reports what the gate actually says |
| 36 | // instead of holding a second opinion that can drift from it. |
| 37 | func (a *App) stillReaches(sheetID, memberID uint64) bool { |
| 38 | m, ok := a.memberByID(memberID) |
| 39 | if !ok { |
| 40 | // The grant is gone and we cannot tell what is left. The plain |
| 41 | // sentence is the one that claims less. |
| 42 | return false |
| 43 | } |
| 44 | _, still := a.roleFor(sheetID, m) |
| 45 | return still |
| 46 | } |
| 47 | |
| 48 | // revokedTeamNotice names the person the notice is about. |
| 49 | // |
| 50 | // Naming them is not decoration: revoking the grant also takes them off |
| 51 | // the list below, so an unnamed sentence would refer to somebody the |
| 52 | // owner can no longer see anywhere on the screen. |
| 53 | // |
| 54 | // The member ID travels in the query and the address does not, which is |
| 55 | // the whole reason this reads the row back. An email address in a |
| 56 | // request line lands in the platform edge's access logs — the same leak |
| 57 | // the share-link token was moved out of one commit earlier — while an |
| 58 | // id is already on this screen in every revoke form's hidden field. |
| 59 | // |
| 60 | // Checked rather than trusted: the id came from a URL, so the sentence |
| 61 | // is personalised only for a member this sheet genuinely still admits. |
| 62 | func (a *App) revokedTeamNotice(r *http.Request, sheetID uint64) string { |
| 63 | id, err := strconv.ParseUint(r.URL.Query().Get("who"), 10, 64) |
| 64 | if err != nil || id == 0 { |
| 65 | return revokedTeamText |
| 66 | } |
| 67 | m, ok := a.memberByID(id) |
| 68 | if !ok || !a.stillReaches(sheetID, m.ID) { |
| 69 | return revokedTeamText |
| 70 | } |
| 71 | return "The explicit permission is removed, but since " + m.Address + |
| 72 | " is on the team, they still have access." |
| 73 | } |
| 74 | |
| 75 | // noticeTone is the rastrillo callout tone for a notice code on the |
| 76 | // share, comments and history pages (shared chrome spec §4). They were |
| 77 | // all one neutral grey callout, so "Access removed." and "Unknown role." |
| 78 | // looked the same. |
| 79 | // |
| 80 | // - positive: the thing asked for happened. |
| 81 | // - negative: it did not, and the form needs something different. |
| 82 | // - warning: it happened, and there is something to know about it. |
| 83 | // - info: nothing happened, and nothing was wrong. |
| 84 | func noticeTone(code string) string { |
| 85 | switch code { |
| 86 | case "shared", "revoked", "link-revoked", "added", "resolved", "restored", "undone": |
| 87 | return "positive" |
| 88 | case "bad-address", "bad-role", "last-owner", "bad-link-role", "empty", "no-cell", "gone": |
| 89 | return "negative" |
| 90 | case noticeRevokedTeam, "link": |
| 91 | return "warning" |
| 92 | } |
| 93 | return "info" |
| 94 | } |
| 95 | |