Files
SocialPub/PrivaPub/Federation/Signing/SignedFetchAuthorizer.cs
T

91 lines
4.2 KiB
C#
Raw Normal View History

using PrivaPub.Federation.Actors;
using PrivaPub.Federation.Objects;
using PrivaPub.Models.Federation;
using PrivaPub.Models.Post;
using PrivaPub.Models.User;
using PrivaPub.StaticServices;
using GroupEntity = PrivaPub.Models.Group.Group;
using PostEntity = PrivaPub.Models.Post.Post;
namespace PrivaPub.Federation.Signing
{
public interface ISignedFetchAuthorizer
{
Task<ForeignAvatar> Requester(HttpRequest request, CancellationToken token);
Task<bool> MayRead(PostEntity post, ForeignAvatar requester, CancellationToken token);
}
// Who may refetch a post that is not public (owner decision 2026-10-04). A server that received a followers-only post,
// a DM or a circle post refetches it, signed by the account it was for or by its instance actor, and Mastodon deletes its
// copy when the refetch answers 404. So the post is served to a signed request from someone it was for, or from the
// instance actor of a server where someone it was for lives; everyone else still gets 404.
public class SignedFetchAuthorizer : ISignedFetchAuthorizer
{
readonly IRemoteActorService _remoteActors;
readonly DbEntities _dbEntities;
public SignedFetchAuthorizer(IRemoteActorService remoteActors, DbEntities dbEntities)
{
_remoteActors = remoteActors;
_dbEntities = dbEntities;
}
public async Task<ForeignAvatar> Requester(HttpRequest request, CancellationToken token)
{
var parameters = HttpSignatures.Parse(request.Headers["Signature"].ToString());
if (parameters == default || HttpSignatures.CheckRequest(request, parameters, body: default) != default)
return default;
var signingString = HttpSignatures.SigningString(request, parameters);
var actor = await _remoteActors.GetActorByKeyId(parameters.KeyId, refresh: false, token);
if (actor != default && HttpSignatures.Verify(actor.PublicKey, signingString, parameters.Signature))
return actor;
actor = await _remoteActors.GetActorByKeyId(parameters.KeyId, refresh: true, token);
return actor != default && HttpSignatures.Verify(actor.PublicKey, signingString, parameters.Signature) ? actor : default;
}
public async Task<bool> MayRead(PostEntity post, ForeignAvatar requester, CancellationToken token)
{
if (post == default || requester == default)
return false;
switch (post.Visibility)
{
case PostVisibility.Public or PostVisibility.Unlisted:
return true;
case PostVisibility.Circle:
var circle = string.IsNullOrEmpty(post.GroupId) ? default : await _dbEntities.Groups.MatchID(post.GroupId).ExecuteFirstAsync(token);
return circle != default && MayReadCircle(circle, requester);
case PostVisibility.Direct:
return Addressed(post).Any(uri => Is(requester, uri));
case PostVisibility.FollowersOnly:
if (Addressed(post).Any(uri => Is(requester, uri)))
return true;
var followers = await _dbEntities.Followers
.Match(f => f.LocalActorId == post.GroupUserId && f.LocalActorKind == LocalActorKind.Person && f.IsAccepted)
.ExecuteAsync(token);
return followers.Any(f => Is(requester, f.ActorURI));
default:
return false;
}
}
static IEnumerable<string> Addressed(PostEntity post) =>
post.To.Concat(post.Cc).Concat(post.Mentions.Select(m => m.ActorURI)).Where(uri => !string.IsNullOrEmpty(uri));
// the account itself, or the instance actor of the server it lives on
static bool Is(ForeignAvatar requester, string actorUri) =>
actorUri == requester.ActorURI || requester.AvatarType == AvatarType.Application && Origin.Same(actorUri, requester.ActorURI);
public static bool MayReadCircle(GroupEntity circle, ForeignAvatar requester) =>
requester != default && circle.Members.Any(m => m.IsForeign && Is(requester, m.AvatarId));
public static bool MayReadConversation(Models.Group.DmGroup conversation, ForeignAvatar requester) =>
requester != default && conversation.Members.Any(m => m.IsForeign && Is(requester, m.AvatarId));
// the members a refetch names in cc: the requesting member, or the members on an instance actor's server
public static IReadOnlyList<string> CircleReaders(GroupEntity circle, ForeignAvatar requester) =>
requester == default
? Array.Empty<string>()
: circle.Members.Where(m => m.IsForeign && Is(requester, m.AvatarId)).Select(m => m.AvatarId).ToList();
}
}