Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[protocol] Questions and remarks about Interaction Policies #3616

Open
ClearlyClaire opened this issue Dec 10, 2024 · 10 comments
Open

[protocol] Questions and remarks about Interaction Policies #3616

ClearlyClaire opened this issue Dec 10, 2024 · 10 comments

Comments

@ClearlyClaire
Copy link

ClearlyClaire commented Dec 10, 2024

Hi!

We are currently working on quote posts in Mastodon, and one essential feature we want is control by the quoted person on who can quote them. Given the similar purposes, we had a look into your Interaction Policy proposal (we are also interested in at least reply controls, but that's something we'll tackle later, and quote posts may be a good way to evaluate that approach in practice and at scale).

In doing so, we have encountered a few shortcomings, possible improvements or questions. I will try to summarize them.

Implicit policies

Our most immediate use case is to represent policies for quoting, which is not defined in your document, but a canQuote policy seems like a natural extension of your proposal.

However, we would not immediately support interaction policies for the other kind of interactions, so it would make sense leaving them out, but according to your documentation, leaving a policy out is equivalent to outright denying interactions, which is not what we want to do.

I suppose we could have as:Public in always, but it slightly bloats the messages while kind of implying that we support those policies in any way, and it brings up the issue of changing the policy after the fact (this is expanded on later in another section).

Revocation of previous approvals

One thing people tend to expect, and that we are intent on allowing wrt. quote posts in Mastodon, is revoking approval for an interaction (see Bluesky's functionality to “detach a quote”).

I suppose it could be done by occasionally re-checking the approvedBy activity or sending and forwarding Reject activities (with no guarantee they would reach everyone who processed the approval in the first place though), but this is not discussed at all in your documentation.

Changing an existing policy for a more restrictive one

Say a post exists without a policy, or with an "always": "https://www.w3.org/ns/activitystreams#Public" policy, and gets some interactions.

Because those interactions are immediately allowed by the always policy, they bypass the approval flow and do not carry an approvedBy attribute.

Now, say the user has changed their mind (or the software they use has been updated to support policies) and do not want any more interactions without approval, specifying a more restrictive policy. From the user's perspective, this should not invalidate existing interactions.

However, already-issued interactions did not need and did not produce an approval activity, and do not refer to one in approvedBy, meaning that third-party servers cannot verify them.

One idea that I had though I don't know how much sense it makes, is mandate that the approval activity, if it exists, can be dereferenced at a deterministic address that would be enough for the approval-granting server to evaluate and grant approval on the fly, e.g.: the authorization for a reply https://reply.example.com/users/123/statuses/456 to a post https://origin.example.com/objects/1234 could be queried at https://origin.example.com/objects/1234/interactions/reply?uri=https%31%2F%2Freply.example.com%2Fusers%2F123%2Fstatuses%2F456

(This would not mandate that this is the identifier of the approval, but that would also be possible)

Ambiguity of approvals

In your proposal, you re-use Accept and Reject to represent approval or rejection of an interaction, relying on actor and object to identify the interaction.

This works for your examples, but can be ambiguous. Indeed, consider a reply that is inReplyTo two different posts are more (this is typically not handled by microblogging software, and in particular not by Mastodon, but this is allowed by the specs and some software support it). How to handle a rejection or an acceptance then becomes ambiguous. Even moreso if multiple posts being replied to are from the same person.

The issue rises in the context of quote posts as well: a quote post could be in reply to another post, and be subjected to two interaction policies of different kinds at once, a reply policy, and a quote policy, and you may want to react differently to a rejection from one or the other. For instance, in our design, a revoked quote approval would mean the quoting post still exists but the quote itself is replaced with a placeholder. But if we are quoting and replying to the same person and get a rejection (or an approval that gets retracted), it is not possible with your proposal to tell which interaction was denied.

Scaling

Your proposal requires every third-party server to fetch an approval for every interacted post, which can generate a lot of traffic if a post gets interacted with by a popular account.

I unfortunately do not have any idea how to improve that, except for the use of a short-lived inline signature to ease with the initial distribution, but it would still behave as badly if the interaction gets shared after the signature's expiry.

At least the approval document can get cached.

@ClearlyClaire
Copy link
Author

Adding a couple questions I thought about when the repository was locked for the holidays:

Approval type swap

An attacker could obtain approval for one kind of interactions (e.g. liking a post) before swapping the activity and type with something else (e.g. a reply). That being said, this is a difficult issue to solve, as having the type hardcoded would not prevent e.g. swapping a reply with another one that has different contents. The only way to fully avoid that kind of things is to sign the accepted activity in the approval, but that comes with many drawbacks.

Private collections in always

Your documentation says valid URIs for always include the followers and following collections. However, in multiple fediverse software projects, the contents of these collections can be private. I was not able to find anything in your documentation that describes how a third-party server can check that an interaction is indeed allowed when such a private collection is in always.

@tsmethurst
Copy link
Contributor

Alright! Back from holidays, gonna have a crack at answering these questions now :) I'll start with the brief ones.

Private collections in always

This one's covered here: https://docs.gotosocial.org/en/latest/federation/posts/#validating-presence-in-a-followers-following-collection Basically, in this case the interacter should wait for approval from the interactee before broadcasting the activity with the Accept URI attached, as there's no other way in the current setup for remotes to validate presence in a followers/following collection. We could implement some FEP or follower synchronization method for this in the future, maybe, but for now this was the simplest way of handling it.

Approval type swap

Yeah it's definitely not watertight, indeed someone could swap the contents of an approved reply for something else. They can also do this using edits if they want to. In future we might want to encourage interacters to revalidate statuses or something by requesting a new Accept from the interactee, or having the interactee put a digest in the Accept activity so that remotes can check what's actually Accepted. Not sure yet.

Scaling

Indeed we rely on in-mem caching and request throttling for this. The situation isn't much different from someone with lots of followers boosting a post. So far we haven't seen any issues with this but it's worth bearing in mind as Mastodon servers (which tend to have a lot more users + users with a lot more followers) also start to adopt interaction policies.

Implicit policies

However, we would not immediately support interaction policies for the other kind of interactions, so it would make sense leaving them out, but according to your documentation, leaving a policy out is equivalent to outright denying interactions, which is not what we want to do.

This is definitely open to changes :) I'm the one who wrote that part of the document, but afaik it's not really being used yet, GtS always just sends everything. I only added it for the sake of trying to eliminate undefined behavior. If you'd rather that leaving a policy out is equivalent to as:public then that could work as well, all good for us!

Revocation of previous approvals

Yeah that's definitely a gap! If I remember rightly in the code we treat Reject as a revocation, and handle side effects appropriately but that's not mentioned in the document. This is something to ponder and something that should definitely be added.

Changing an existing policy for a more restrictive one

I guess this is similar to revocation in that if you're changing to a more restrictive policy, we'll have to think about this one a bit more as it's quite tricky indeed, especially the point you raise about interactions that are implicitly permitted and therefore don't carry an Accept URI with them.

One way of handling this, I suppose, would be for the interactee's server, when the policy changes, to send out Accepts to all interacters whose interactions were already implicitly accepted due to as:public, so that the Accept URIs could be added to those interactions at the moment the policy changes, allowing those interactions to remain.

Alternatively, though this creates a lot more traffic, would be for the interactee to always generate + send out an Accept even for interactions that are permitted implicitly.

A third (much less preferable) option would be for the interactee's server to send out Rejects to all interacters whose interactions would be rejected by the new policy, essentially "clearing" those interactions. I don't really like this though. When you close discussion on an issue or a forum or whatever, you don't wanna delete the history of the discussion :') At least, not usually.

a deterministic address that would be enough for the approval-granting server to evaluate and grant approval on the fly

I was toying with a similar idea, I think maybe we discussed this on the fedi or on discord at some point? I don't quite remember. Ideally I'd like to avoid introducing a new endpoint if possible, as it requires further protocol extensions. But maybe it will be necessary. Not sure!

Ambiguity of approvals

Yeah! This is similar to something mentioned here: #3460 Basically I wanted to avoid introducing types like AcceptReply, RejectReply, AcceptLike, AcceptAnnounce etc etc as it's just a bloody mess when you consider all the different types of interactions you might want to accept or reject. But perhaps there's a neat way of hinting within an Accept or Reject exactly what you're Accepting or Rejecting.

@ClearlyClaire
Copy link
Author

ClearlyClaire commented Jan 20, 2025

FYI our current work-in-progress draft for quote posts uses a top-level approvesQuotesFrom attribute that kind of works like your interactionPolicy.*.approvalRequired, avoiding the confusing nesting as well as mixing the policy and verification.

Private collections in always

This one's covered here: https://docs.gotosocial.org/en/latest/federation/posts/#validating-presence-in-a-followers-following-collection […]

Oops yeah, I missed it indeed. Fundamentally, your proposal does cover this. Maybe it can be worded a bit more clearly and call this out earlier, but maybe that's just a me problem!

Scaling

Indeed we rely on in-mem caching and request throttling for this. The situation isn't much different from someone with lots of followers boosting a post. So far we haven't seen any issues with this but it's worth bearing in mind as Mastodon servers (which tend to have a lot more users + users with a lot more followers) also start to adopt interaction policies.

It is a bit similar, but on first discovery you need two requests (post + interaction approval), and for every subsequent use you need a request, even if you know the original post. Which is definitely more than with boosts.

Implicit policies

However, we would not immediately support interaction policies for the other kind of interactions, so it would make sense leaving them out, but according to your documentation, leaving a policy out is equivalent to outright denying interactions, which is not what we want to do.

This is definitely open to changes :) I'm the one who wrote that part of the document, but afaik it's not really being used yet, GtS always just sends everything. I only added it for the sake of trying to eliminate undefined behavior. If you'd rather that leaving a policy out is equivalent to as:public then that could work as well, all good for us!

This one is a bit tricky, but what we think is:

  • nesting the policy declarations is not useful
  • what the default should be might depend on the interaction type; for instance, it's established in the fediverse that Announcements do not require an approval, so it makes sense for the public collection to be the default, but if the policy and approval mechanisms become widespread, it may make sense for new interaction types to be disallowed by default

Changing an existing policy for a more restrictive one

I think it's actually not very similar to revocation, as you'd likely not want to automatically revoke existing approved interactions on policy change. This would be your third option, and I agree this is much less preferable.

Your first option does make a lot of sense although it could create a big burst of traffic.

Your second option would also make sense and that's actually what I would suggest (unless we're adding a new endpoint, see next point), making the policy entirely advisory, and the enforcement a separate concern.

a deterministic address that would be enough for the approval-granting server to evaluate and grant approval on the fly

I was toying with a similar idea, I think maybe we discussed this on the fedi or on discord at some point? I don't quite remember. Ideally I'd like to avoid introducing a new endpoint if possible, as it requires further protocol extensions. But maybe it will be necessary. Not sure!

The proposed flow, especially with “DO NOT DISTRIBUTE THE ACTIVITY FURTHER THAN THIS AT THIS POINT.”, is already a pretty big protocol extension. There's a not-completely-satisfactory way out of it by allowing Updateing a post that isn't approved once it is approved.

I'm kind of in favor of introducing a new endpoint since it could streamline things quite a bit, but also I'm not sure how it would work with C2S yet.

Ambiguity of approvals

Yeah! This is similar to something mentioned here: #3460 Basically I wanted to avoid introducing types like AcceptReply, RejectReply, AcceptLike, AcceptAnnounce etc etc as it's just a bloody mess when you consider all the different types of interactions you might want to accept or reject. But perhaps there's a neat way of hinting within an Accept or Reject exactly what you're Accepting or Rejecting.

I understand wanting to avoid adding those, although they are not more “expensive” from a compatibility viewpoint, and avoid potential confusion.

We can probably use target to make it slightly more specific (targeting the object the activity is interacting with), but we'd probably also need a purpose to distinguish the type of interaction. But even if we do this and mandate verifying this, I'm slightly worried other implementations could handle the Accepts for other usages without checking those extra properties.

@tsmethurst
Copy link
Contributor

tsmethurst commented Jan 20, 2025

nesting the policy declarations is not useful

I suppose you could also flatten it to something like this:

{
  [ ... blah blah blah the rest of the Note ... ]
  interactionPolicy: {
    "canLikeAlways": [ "zero_or_more_uris_that_can_always_do_this" ],
    "canLikeApprovalRequired": [ "zero_or_more_uris_that_require_approval_to_do_this" ],
    [ ... blah blah blah the rest of the things in the same pattern ... ]
  }
}

But I wouldn't put things like canLikeAlways etc on the top level of the note, personally, as it's easier to check for awareness of interaction policies by just looking for the presence of the one interactionPolicy key in the JSON than be checking for a bunch of different ones. That was a purposeful decision. Besides, it's not like nesting JSON costs anything really.

what the default should be might depend on the interaction type

I'm very wary of this as I think it's going to introduce a much higher possibility of different implementers getting their wires crossed trying to remember what is implicitly permitted and what is implicitly not permitted. It also relies on and codifies social norms which I don't think should be relied on or codified. I think protocol extensions should try to stay neutral and technical where possible, ie., "if a key is omitted the interaction is always permitted" or "if a key is omitted the interaction is always forbidden" and not "if the Like key is omitted it's always permitted because likes are 'less of a big deal', if the Reply key is omitted it's always forbidden because replies are 'more of a big deal'" etc. Does that make sense?

it's established in the fediverse that Announcements do not require an interaction type

I don't really agree with this: in my experience people often post things on followers-only not because they necessarily want to limit views to only their followers, but because they don't want stuff getting boosted all over the place and attracting attention outside their bubble. In that light, folks definitely recognize announcements as a form of interaction that they might want to limit.

Your second option would also make sense and that's actually what I would suggest (unless we're adding a new endpoint, see next point), making the policy entirely advisory, and the enforcement a separate concern.

Second option is fine then I think :) It also helps with distributing replies, which is a nice bonus.

making the policy entirely advisory

Tbf it's always going to be advisory, we can't stop other implementations from doing whatever they want :P

We can probably use target to make it slightly more specific (targeting the object the activity is interacting with)

I think this would definitely be enough to hint to the receiving instance about how they should handle the Accept. I'd be totally OK with adding this!

even if we do this and mandate verifying this, I'm slightly worried other implementations could handle the Accepts for other usages without checking those extra properties.

They might yeah, you never know. But afaik most other implementations use Accept to look at Follow (requests) only, I think. Prior to implementing interaction policies, GtS would just look for the target of an Accept in the database, and if it didn't find anything, would return 202 and do nothing else. I imagine that's what other implementations are already doing with the Accepts GtS is now sending out in response to interactions. I haven't heard of it causing any issues yet 🤞

@ClearlyClaire
Copy link
Author

But I wouldn't put things like canLikeAlways etc on the top level of the note, personally, as it's easier to check for awareness of interaction policies by just looking for the presence of the one interactionPolicy key in the JSON than be checking for a bunch of different ones. That was a purposeful decision. Besides, it's not like nesting JSON costs anything really.

Checking the keys of the root object is also not more complicated than checking the keys of interactionPolicy, and it avoids possible discrepancies in defaults handling between not having interactionPolicy at all and not having interactionPolicy.canLike.

it's established in the fediverse that Announcements do not require an interaction type

I don't really agree with this: in my experience people often post things on followers-only not because they necessarily want to limit views to only their followers, but because they don't want stuff getting boosted all over the place and attracting attention outside their bubble. In that light, folks definitely recognize announcements as a form of interaction that they might want to limit.

Oops, sorry, I thought something but wrote something else (edited the post afterwards): I meant it's established in the fediverse (as in, every implementation for years) that Announce activities do not require an approval.

So it makes sense to me that in the absence of a canAnnounce, whether it's an absence of interactionPolicy or interactionPolicy.canAnnounce, means Announce-ing is allowed.

I definitely think the ability to set a policy for announces can be useful.

I think this would definitely be enough to hint to the receiving instance about how they should handle the Accept. I'd be totally OK with adding this!

It would still not be enough if we e.g. have something that is both in-reply-to and quoting the same post, and we want to distinguish the two abilities. This might sound very artificial, but being able to tell the two apart makes sense if you consider the ability to change a policy without invalidating previous interactions: it may not be possible to distinguish an adversary making double-use of an approval, and a legitimate use of e.g. quote before the quoted user changed their policy.

@ClearlyClaire
Copy link
Author

making the policy entirely advisory

Tbf it's always going to be advisory, we can't stop other implementations from doing whatever they want :P

Sure, but I meant the policy itself would never be used for verification, which would be a separate concern.

I think protocol extensions should try to stay neutral and technical where possible, ie., "if a key is omitted the interaction is always permitted" or "if a key is omitted the interaction is always forbidden" and not "if the Like key is omitted it's always permitted because likes are 'less of a big deal', if the Reply key is omitted it's always forbidden because replies are 'more of a big deal'" etc. Does that make sense?

That makes sense, but also the established technical point is that the likes and replies do not require permission, that's the expectation for implementations that do not implement that extension, so codifying the opposite seems weird and unpractical compatibility-wise.

But I understand your position, and, for instance, in Mastodon we do not plan on supporting quoting posts that do not advertise a policy, because quotes are new and existing posts are not.

@tsmethurst
Copy link
Contributor

tsmethurst commented Jan 22, 2025

Right! I think I finally understand what you're getting at, my bad.

So what you're saying is, when interactionPolicy is undefined, or interactionPolicy.canLike, interactionPolicy.canReply, and interactionPolicy.canAnnounce are undefined, then implementations should assume that existing pre-interaction-policy interactions are implicitly permitted, right? So if you see a Note or whatever with a policy like this:

"interactionPolicy": {
  "canReply": {
    "always": [ "own_actor_uri", "some_followers_uri" ]
    "approvalRequired": [ "magic_public_uri" ]
  }
}

Then you should assume that only followers can reply straight up, and the rest requires approval, but likes and boosts are implicitly approved, right?

And then quote posting is special cased so that even though likes and boosts are implicitly approved, quote posts are not.

Am I understanding that correctly? I think that would be more or less OK tbh.

I think we'll end up differing here in that with GtS we'll probably always transmit the whole policy to make things explicit, but as long as we're working from the same assumptions it's not an issue for cross-compatibility. We'll just have to be very careful when writing this up to make things as clear as possible.

@ClearlyClaire
Copy link
Author

Then you should assume that only followers can reply straight up, and the rest requires approval, but likes and boosts are implicitly approved, right?

And then quote posting is special cased so that even though likes and boosts are implicitly approved, quote posts are not.

Am I understanding that correctly?

Yes, that's what I meant.

As for your example, I'd still remove the distinction between always and approvalRequired, since, as we discussed earlier, even with always you should rely on the explicit approval mechanism.

@ClearlyClaire
Copy link
Author

One issue I've been pointed out is that when canonicalizing a JSON-LD document, empty arrays just disappear, so this means with Mastodon's current signature scheme, we cannot assign different values between “missing property” and “empty array”… this is very troublesome.

An ugly workaround could be to never leave those attributes empty, by always putting the actor's URI in there, since they are always implicitly allowed to interact with themselves.

@tsmethurst
Copy link
Contributor

An ugly workaround could be to never leave those attributes empty, by always putting the actor's URI in there, since they are always implicitly allowed to interact with themselves.

That's prettymuch what GtS does already :) viva ugliness! 🦥

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants