Merge pull request #3937 from vitorpamplona/fix/relay-auth-always

fix: honour "always log in" for relays the account does not use itself
This commit is contained in:
Vitor Pamplona
2026-08-16 22:51:54 -04:00
committed by GitHub
2 changed files with 28 additions and 10 deletions
@@ -92,11 +92,14 @@ data class RelayAuthInputs(
* else fall through
* 4. Fall-through → [RelayAuthVerdict.ASK] when the purpose is known, otherwise DENY.
*
* Both automatic grants in step 3 additionally require [RelayAuthInputs.isFirstParty]: an account
* never reveals its identity *without being asked* on a relay it has no reason of its own to be on.
* That is what keeps a bystander account off a relay only another account uses. It deliberately does
* not suppress the question — a non-first-party challenge we can explain falls through to ASK, so
* "decide per relay" means the user decides rather than a silent denial they never see.
* The [RelayAuthPolicy.CUSTOM] grant additionally requires [RelayAuthInputs.isFirstParty]: under
* "decide per relay" an account never reveals its identity *without being asked* on a relay it has no
* reason of its own to be on, which is what keeps a bystander account off a relay only another account
* uses. It deliberately does not suppress the question — a non-first-party challenge we can explain
* falls through to ASK, so the user decides rather than getting a silent denial they never see.
*
* [RelayAuthPolicy.ALWAYS] is NOT gated this way: it means what it says, every relay that asks. Users
* who want the narrower "only the relays I actually use" behaviour choose CUSTOM.
*/
object RelayAuthResolver {
fun resolve(inputs: RelayAuthInputs): RelayAuthVerdict {
@@ -111,7 +114,11 @@ object RelayAuthResolver {
return when (inputs.policy) {
RelayAuthPolicy.NEVER -> RelayAuthVerdict.DENY
RelayAuthPolicy.ALWAYS -> if (inputs.isFirstParty) RelayAuthVerdict.ALLOW else fallThrough(inputs)
// Unconditional, by design: "Always log in" means every relay that asks. Narrowing it to the
// relays this account uses is what CUSTOM ("decide per relay") is for — gating ALWAYS as well
// left the user no way to express "just authenticate everywhere" and, on a fresh install with
// a large follow list, produced a prompt for each of the 250+ third-party outbox relays.
RelayAuthPolicy.ALWAYS -> RelayAuthVerdict.ALLOW
RelayAuthPolicy.CUSTOM ->
if (inputs.isFirstParty && customAllows(inputs)) RelayAuthVerdict.ALLOW else fallThrough(inputs)
}
@@ -146,11 +146,22 @@ class RelayAuthResolverTest {
}
@Test
fun nonFirstPartyNeverAutoAuthsUnderAlwaysPolicy() {
// "Always log in" is a statement about the relays this account uses. A relay it is only
// touching because of somebody else's traffic still has to be asked about.
fun alwaysPolicyAuthsEvenWhenNotFirstParty() {
// "Always log in" means every relay that asks, first-party or not — narrowing it to the relays
// this account uses is what CUSTOM is for. (This previously returned ASK for non-first-party,
// which left no way to express "authenticate everywhere" and prompted once per third-party
// outbox relay on a fresh install.)
assertEquals(RelayAuthVerdict.ALLOW, resolve(inputs(policy = RelayAuthPolicy.ALWAYS, isFirstParty = true)))
assertEquals(RelayAuthVerdict.ASK, resolve(inputs(policy = RelayAuthPolicy.ALWAYS, isFirstParty = false)))
assertEquals(RelayAuthVerdict.ALLOW, resolve(inputs(policy = RelayAuthPolicy.ALWAYS, isFirstParty = false)))
}
@Test
fun customPolicyStillRequiresFirstParty() {
// The first-party gate belongs to CUSTOM: a toggle that matches is not enough if the only reason
// we are on this relay belongs to somebody else.
val allOn = RelayAuthCustomToggles(myRelaysAndVenues = true, readFollows = true, messageFollows = true, messageStrangers = true)
assertEquals(RelayAuthVerdict.ALLOW, resolve(inputs(toggles = allOn, isInMyRelayList = true, isFirstParty = true)))
assertEquals(RelayAuthVerdict.ASK, resolve(inputs(toggles = allOn, isInMyRelayList = true, isFirstParty = false)))
}
@Test