mirror of
https://github.com/RetroShare/libretroshare.git
synced 2026-09-12 19:50:03 +05:00
Implement admin signature handling and pinning functionality for posted items
This commit is contained in:
parent
2f093fd7b3
commit
4d6005b646
@ -531,6 +531,31 @@ bool GxsSecurity::validateNxsMsg(const RsNxsMsg& msg, const RsTlvKeySignature& s
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GxsSecurity::getAdminSignature(const char* data, uint32_t size,
|
||||
const RsTlvSecurityKeySet& keys,
|
||||
RsTlvKeySignature& signature)
|
||||
{
|
||||
for(const auto& entry : keys.private_keys)
|
||||
if(entry.second.keyFlags & RSTLV_KEY_DISTRIB_ADMIN)
|
||||
return getSignature(data, size, entry.second, signature);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GxsSecurity::validateAdminSignature(const RsNxsMsg& msg,
|
||||
const RsTlvSecurityKeySet& keys)
|
||||
{
|
||||
if(!msg.metaData) return false;
|
||||
const auto found = msg.metaData->signSet.keySignSet.find(ADMIN_SIGNATURE_INDEX);
|
||||
if(found == msg.metaData->signSet.keySignSet.end()) return false;
|
||||
// validateNxsMsg temporarily clears and restores the metadata signatures.
|
||||
const RsTlvKeySignature signature = found->second;
|
||||
for(const auto& entry : keys.public_keys)
|
||||
if((entry.second.keyFlags & RSTLV_KEY_DISTRIB_ADMIN)
|
||||
&& validateNxsMsg(msg, signature, entry.second))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GxsSecurity::encrypt(uint8_t *& out, uint32_t &outlen, const uint8_t *in, uint32_t inlen, const RsTlvPublicRSAKey& key)
|
||||
{
|
||||
#ifdef DISTRIB_DEBUG
|
||||
|
||||
@ -95,6 +95,13 @@ class GxsSecurity
|
||||
*/
|
||||
static bool validateNxsMsg(const RsNxsMsg& msg, const RsTlvKeySignature& sign, const RsTlvPublicRSAKey &key);
|
||||
|
||||
static constexpr uint32_t ADMIN_SIGNATURE_INDEX = 0x00000040;
|
||||
static bool getAdminSignature(const char* data, uint32_t size,
|
||||
const RsTlvSecurityKeySet& keys,
|
||||
RsTlvKeySignature& signature);
|
||||
static bool validateAdminSignature(const RsNxsMsg& msg,
|
||||
const RsTlvSecurityKeySet& keys);
|
||||
|
||||
|
||||
/*!
|
||||
* @param data data to be signed
|
||||
|
||||
@ -61,7 +61,7 @@
|
||||
|
||||
static const uint32_t INDEX_AUTHEN_IDENTITY = 0x00000010; // identity
|
||||
static const uint32_t INDEX_AUTHEN_PUBLISH = 0x00000020; // publish key
|
||||
static const uint32_t INDEX_AUTHEN_ADMIN = 0x00000040; // admin key
|
||||
static const uint32_t INDEX_AUTHEN_ADMIN = GxsSecurity::ADMIN_SIGNATURE_INDEX;
|
||||
|
||||
static const uint32_t MSG_CLEANUP_PERIOD = 60*59; // 59 minutes
|
||||
static const uint32_t INTEGRITY_CHECK_PERIOD = 60*31; // 31 minutes
|
||||
@ -643,6 +643,16 @@ int RsGenExchange::createGroupSignatures(RsTlvKeySignatureSet& signSet, RsTlvBin
|
||||
int RsGenExchange::createMsgSignatures(RsTlvKeySignatureSet& signSet, RsTlvBinaryData& msgData,
|
||||
const RsGxsMsgMetaData& msgMeta, const RsGxsGrpMetaData& grpMeta)
|
||||
{
|
||||
if(service_requiresAdminSignature(msgMeta))
|
||||
{
|
||||
RsTlvKeySignature signature;
|
||||
if(!GxsSecurity::getAdminSignature(
|
||||
static_cast<const char*>(msgData.bin_data), msgData.bin_len,
|
||||
grpMeta.keys, signature))
|
||||
return SIGN_FAIL;
|
||||
signSet.keySignSet[INDEX_AUTHEN_ADMIN] = signature;
|
||||
}
|
||||
|
||||
uint32_t grpFlag = grpMeta.mGroupFlags;
|
||||
|
||||
#ifdef GEN_EXCH_DEBUG
|
||||
@ -835,6 +845,10 @@ int RsGenExchange::createMessage(RsNxsMsg* msg)
|
||||
|
||||
int RsGenExchange::validateMsg(RsNxsMsg *msg, const uint32_t& grpFlag, const uint32_t& /*signFlag*/, RsTlvSecurityKeySet& grpKeySet)
|
||||
{
|
||||
if(service_requiresAdminSignature(*msg->metaData)
|
||||
&& !GxsSecurity::validateAdminSignature(*msg, grpKeySet))
|
||||
return VALIDATE_FAIL;
|
||||
|
||||
// 1 - determine which signatures are needed, by looking for the flags corresponding to the
|
||||
// type of message we have, in the authentication policy of the service
|
||||
|
||||
|
||||
@ -636,9 +636,14 @@ public:
|
||||
*/
|
||||
void deleteMsgs(uint32_t& token, const GxsMsgReq& msgs);
|
||||
|
||||
protected:
|
||||
/*!
|
||||
* This represents the group before its signature is calculated
|
||||
protected:
|
||||
/** Additional message authorization, enforced on both publishing and
|
||||
* receiving. Services opt in for operations reserved to group admins. */
|
||||
virtual bool service_requiresAdminSignature(const RsGxsMsgMetaData&) const
|
||||
{ return false; }
|
||||
|
||||
/*!
|
||||
* This represents the group before its signature is calculated
|
||||
* Reimplement this function if you need to access keys to further extend
|
||||
* security of your group items using keyset properties
|
||||
* Derived service should return one of three ServiceCreate_Return enum values below
|
||||
|
||||
@ -32,6 +32,7 @@
|
||||
#include "retroshare/rsgxscommon.h"
|
||||
#include "retroshare/rsgxscircles.h"
|
||||
#include "serialiser/rsserializable.h"
|
||||
#include "serialiser/rstlvidset.h"
|
||||
|
||||
class RsPosted;
|
||||
|
||||
@ -45,6 +46,7 @@ struct RsPostedGroup: public RsSerializable, RsGxsGenericGroupData
|
||||
{
|
||||
std::string mDescription;
|
||||
RsGxsImage mGroupImage;
|
||||
RsTlvGxsMsgIdSet mPinnedPosts;
|
||||
|
||||
/// @see RsSerializable
|
||||
virtual void serial_process( RsGenericSerializer::SerializeJob j,
|
||||
@ -53,6 +55,7 @@ struct RsPostedGroup: public RsSerializable, RsGxsGenericGroupData
|
||||
RS_SERIAL_PROCESS(mMeta);
|
||||
RS_SERIAL_PROCESS(mDescription);
|
||||
RS_SERIAL_PROCESS(mGroupImage);
|
||||
RS_SERIAL_PROCESS(mPinnedPosts);
|
||||
}
|
||||
};
|
||||
|
||||
@ -66,6 +69,10 @@ struct RsPostedPost: public RsSerializable, RsGxsGenericMsgData
|
||||
std::string mLink;
|
||||
std::string mNotes;
|
||||
|
||||
// Latest revision shown by the blocking content APIs; mMeta retains the
|
||||
// original thread identity, author, timestamp, read status and vote cache.
|
||||
RsGxsMessageId mRevisionId;
|
||||
|
||||
bool mHaveVoted;
|
||||
|
||||
// Calculated.
|
||||
@ -89,6 +96,7 @@ struct RsPostedPost: public RsSerializable, RsGxsGenericMsgData
|
||||
RS_SERIAL_PROCESS(mMeta);
|
||||
RS_SERIAL_PROCESS(mLink);
|
||||
RS_SERIAL_PROCESS(mNotes);
|
||||
RS_SERIAL_PROCESS(mRevisionId);
|
||||
RS_SERIAL_PROCESS(mHaveVoted);
|
||||
RS_SERIAL_PROCESS(mUpVotes);
|
||||
RS_SERIAL_PROCESS(mDownVotes);
|
||||
@ -310,6 +318,8 @@ public:
|
||||
* @param[in] image optional post image.
|
||||
* @param[out] postId id of the post after it's been generated
|
||||
* @param[out] error_message possible error message if the method returns false
|
||||
* @param[in] origPostId Original post to edit; null creates a new post.
|
||||
* Editing requires the board administrator key.
|
||||
* @return true if ok, false if an error occured (see error_message)
|
||||
*/
|
||||
virtual bool createPostV2(const RsGxsGroupId& boardId,
|
||||
@ -319,7 +329,15 @@ public:
|
||||
const RsGxsId& authorId,
|
||||
const RsGxsImage& image,
|
||||
RsGxsMessageId& postId,
|
||||
std::string& error_message) =0;
|
||||
std::string& error_message,
|
||||
const RsGxsMessageId& origPostId = RsGxsMessageId()) =0;
|
||||
|
||||
/** Pin or unpin a post for everyone. Blocking, administrator-only API.
|
||||
* @jsonapi{development}
|
||||
*/
|
||||
virtual bool setPostPinned(const RsGxsGroupId& boardId,
|
||||
const RsGxsMessageId& postId, bool pinned,
|
||||
std::string& errorMessage) =0;
|
||||
|
||||
/** @brief Add a comment on a post or on another comment. Blocking API.
|
||||
* @jsonapi{development}
|
||||
|
||||
@ -49,10 +49,20 @@ void RsGxsPostedGroupItem::serial_process(RsGenericSerializer::SerializeJob j,Rs
|
||||
if(j == RsGenericSerializer::DESERIALIZE && ctx.mOffset == ctx.mSize)
|
||||
return ;
|
||||
|
||||
if((j == RsGenericSerializer::SIZE_ESTIMATE || j == RsGenericSerializer::SERIALIZE) && mGroupImage.empty())
|
||||
if((j == RsGenericSerializer::SIZE_ESTIMATE || j == RsGenericSerializer::SERIALIZE)
|
||||
&& mGroupImage.empty() && mPinnedPosts.ids.empty())
|
||||
return ;
|
||||
|
||||
RsTypeSerializer::serial_process<RsTlvItem>(j,ctx,mGroupImage,"mGroupImage") ;
|
||||
|
||||
// Older groups end after the description or image. With pins, even an
|
||||
// empty image must be written so the trailing fields remain unambiguous.
|
||||
if(j == RsGenericSerializer::DESERIALIZE && ctx.mOffset == ctx.mSize)
|
||||
return;
|
||||
if((j == RsGenericSerializer::SIZE_ESTIMATE || j == RsGenericSerializer::SERIALIZE)
|
||||
&& mPinnedPosts.ids.empty())
|
||||
return;
|
||||
RsTypeSerializer::serial_process<RsTlvItem>(j,ctx,mPinnedPosts,"mPinnedPosts");
|
||||
}
|
||||
|
||||
RsItem *RsGxsPostedSerialiser::create_item(uint16_t service_id,uint8_t item_subtype) const
|
||||
@ -119,6 +129,7 @@ void RsGxsPostedGroupItem::clear()
|
||||
{
|
||||
mDescription.clear();
|
||||
mGroupImage.TlvClear();
|
||||
mPinnedPosts.TlvClear();
|
||||
}
|
||||
|
||||
bool RsGxsPostedGroupItem::fromPostedGroup(RsPostedGroup &group, bool moveImage)
|
||||
@ -126,6 +137,7 @@ bool RsGxsPostedGroupItem::fromPostedGroup(RsPostedGroup &group, bool moveImage)
|
||||
clear();
|
||||
meta = group.mMeta;
|
||||
mDescription = group.mDescription;
|
||||
mPinnedPosts = group.mPinnedPosts;
|
||||
|
||||
if (moveImage)
|
||||
{
|
||||
@ -144,6 +156,7 @@ bool RsGxsPostedGroupItem::toPostedGroup(RsPostedGroup &group, bool moveImage)
|
||||
{
|
||||
group.mMeta = meta;
|
||||
group.mDescription = mDescription;
|
||||
group.mPinnedPosts = mPinnedPosts;
|
||||
if (moveImage)
|
||||
{
|
||||
group.mGroupImage.take((uint8_t *) mGroupImage.binData.bin_data, mGroupImage.binData.bin_len);
|
||||
|
||||
@ -48,6 +48,7 @@ public:
|
||||
|
||||
std::string mDescription;
|
||||
RsTlvImage mGroupImage;
|
||||
RsTlvGxsMsgIdSet mPinnedPosts;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@ -20,6 +20,8 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
#include "services/p3posted.h"
|
||||
#include "services/postedversions.h"
|
||||
#include "gxs/gxssecurity.h"
|
||||
#include "retroshare/rsgxscircles.h"
|
||||
#include "retroshare/rspeers.h"
|
||||
#include "rsitems/rsposteditems.h"
|
||||
@ -69,6 +71,12 @@ bool p3Posted::groupShareKeys(const RsGxsGroupId& groupId,const std::set<RsPeerI
|
||||
return true ;
|
||||
}
|
||||
|
||||
bool p3Posted::service_requiresAdminSignature(const RsGxsMsgMetaData& meta) const
|
||||
{
|
||||
return meta.mParentId.isNull() && !meta.mOrigMsgId.isNull()
|
||||
&& meta.mOrigMsgId != meta.mMsgId;
|
||||
}
|
||||
|
||||
bool p3Posted::getGroupData(const uint32_t &token, std::vector<RsPostedGroup> &groups)
|
||||
{
|
||||
std::vector<RsGxsGrpItem*> grpData;
|
||||
@ -120,6 +128,32 @@ bool p3Posted::getPostData(
|
||||
for(; mit != msgData.end(); ++mit)
|
||||
{
|
||||
std::vector<RsGxsMsgItem*>& msgItems = mit->second;
|
||||
|
||||
// Recheck revisions already in the database too: older clients could
|
||||
// store edits before admin signatures were mandatory on receipt.
|
||||
GxsMsgReq revisionIds;
|
||||
for(const auto* item : msgItems)
|
||||
if(dynamic_cast<const RsGxsPostedPostItem*>(item)
|
||||
&& !item->meta.mOrigMsgId.isNull()
|
||||
&& item->meta.mOrigMsgId != item->meta.mMsgId)
|
||||
revisionIds[mit->first].insert(item->meta.mMsgId);
|
||||
std::set<RsGxsMessageId> authenticated;
|
||||
if(!revisionIds.empty())
|
||||
{
|
||||
RsTlvSecurityKeySet keys;
|
||||
if(getGroupKeys(mit->first, keys))
|
||||
{
|
||||
GxsMsgResult revisions;
|
||||
getDataStore()->retrieveNxsMsgs(revisionIds, revisions, true);
|
||||
for(auto& group : revisions)
|
||||
for(auto* revision : group.second)
|
||||
{
|
||||
if(revision && GxsSecurity::validateAdminSignature(*revision, keys))
|
||||
authenticated.insert(revision->msgId);
|
||||
delete revision;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::vector<RsGxsMsgItem*>::iterator vit = msgItems.begin();
|
||||
|
||||
for(; vit != msgItems.end(); ++vit)
|
||||
@ -129,6 +163,14 @@ bool p3Posted::getPostData(
|
||||
|
||||
if(postItem)
|
||||
{
|
||||
const auto& meta = postItem->meta;
|
||||
if(!meta.mParentId.isNull()
|
||||
|| (!meta.mOrigMsgId.isNull() && meta.mOrigMsgId != meta.mMsgId
|
||||
&& !authenticated.count(meta.mMsgId)))
|
||||
{
|
||||
delete postItem;
|
||||
continue;
|
||||
}
|
||||
// TODO Really needed all of these lines?
|
||||
RsPostedPost msg = postItem->mPost;
|
||||
msg.mMeta = postItem->meta;
|
||||
@ -380,7 +422,9 @@ bool p3Posted::getBoardAllContent( const RsGxsGroupId& groupId,
|
||||
if( !requestMsgInfo(token, opts, std::list<RsGxsGroupId>({groupId})) || waitToken(token) != RsTokenService::COMPLETE )
|
||||
return false;
|
||||
|
||||
return getPostData(token, posts, comments, votes);
|
||||
if(!getPostData(token, posts, comments, votes)) return false;
|
||||
PostedVersions::resolve(posts);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool p3Posted::getRelatedComments( const RsGxsGroupId& gid,const std::set<RsGxsMessageId>& messageIds, std::vector<RsGxsComment> &comments )
|
||||
@ -405,6 +449,7 @@ bool p3Posted::getBoardContent( const RsGxsGroupId& groupId,
|
||||
std::vector<RsGxsComment>& comments,
|
||||
std::vector<RsGxsVote>& votes )
|
||||
{
|
||||
if(contentsIds.empty()) return getBoardAllContent(groupId, posts, comments, votes);
|
||||
uint32_t token;
|
||||
RsTokReqOptions opts;
|
||||
opts.mReqType = GXS_REQUEST_TYPE_MSG_DATA;
|
||||
@ -412,10 +457,24 @@ bool p3Posted::getBoardContent( const RsGxsGroupId& groupId,
|
||||
GxsMsgReq msgIds;
|
||||
msgIds[groupId] = contentsIds;
|
||||
|
||||
// Include every revision of requested posts, but leave comment/vote
|
||||
// requests alone. Fetch metadata first to avoid loading the whole board.
|
||||
std::vector<RsMsgMetaData> summaries;
|
||||
if(!getBoardPostSummaries(groupId, summaries)) return false;
|
||||
std::set<RsGxsMessageId> roots;
|
||||
for(const auto& meta : summaries)
|
||||
if(contentsIds.count(meta.mMsgId))
|
||||
roots.insert(meta.mOrigMsgId.isNull() ? meta.mMsgId : meta.mOrigMsgId);
|
||||
msgIds[groupId].insert(roots.begin(), roots.end());
|
||||
for(const auto& meta : summaries)
|
||||
if(roots.count(meta.mOrigMsgId)) msgIds[groupId].insert(meta.mMsgId);
|
||||
|
||||
if( !requestMsgInfo(token, opts, msgIds) ||
|
||||
waitToken(token) != RsTokenService::COMPLETE ) return false;
|
||||
|
||||
return getPostData(token, posts, comments, votes);
|
||||
if(!getPostData(token, posts, comments, votes)) return false;
|
||||
PostedVersions::resolve(posts);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool p3Posted::getBoardPostSummaries(
|
||||
@ -798,13 +857,16 @@ bool p3Posted::createPostV2(const RsGxsGroupId& boardId,
|
||||
const RsGxsId& authorId,
|
||||
const RsGxsImage& image,
|
||||
RsGxsMessageId& postId,
|
||||
std::string& error_message)
|
||||
std::string& error_message,
|
||||
const RsGxsMessageId& origPostId)
|
||||
{
|
||||
error_message.clear();
|
||||
// check boardId
|
||||
|
||||
std::vector<RsPostedGroup> groupsInfo;
|
||||
|
||||
if(!getBoardsInfo( { boardId }, groupsInfo))
|
||||
if(boardId.isNull() || !getBoardsInfo( { boardId }, groupsInfo)
|
||||
|| groupsInfo.size() != 1)
|
||||
{
|
||||
error_message = "Board with Id " + boardId.toStdString() + " does not exist.";
|
||||
RsErr() << error_message;
|
||||
@ -821,6 +883,31 @@ bool p3Posted::createPostV2(const RsGxsGroupId& boardId,
|
||||
}
|
||||
|
||||
RsPostedPost post;
|
||||
if(title.empty())
|
||||
{
|
||||
error_message = "Please add a title.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!origPostId.isNull())
|
||||
{
|
||||
if(!IS_GROUP_ADMIN(groupsInfo.front().mMeta.mSubscribeFlags))
|
||||
{
|
||||
error_message = "Only a board administrator can edit posts.";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<RsPostedPost> originals;
|
||||
std::vector<RsGxsComment> comments;
|
||||
std::vector<RsGxsVote> votes;
|
||||
if(!getBoardContent(boardId, {origPostId}, originals, comments, votes)
|
||||
|| originals.size() != 1)
|
||||
{
|
||||
error_message = "The original post is not available locally.";
|
||||
return false;
|
||||
}
|
||||
post.mMeta.mOrigMsgId = originals.front().mMeta.mMsgId;
|
||||
}
|
||||
post.mMeta.mGroupId = boardId;
|
||||
post.mLink = link.toString();
|
||||
post.mImage = image;
|
||||
@ -833,13 +920,64 @@ bool p3Posted::createPostV2(const RsGxsGroupId& boardId,
|
||||
RsGenericSerializer::SerializeContext ctx;
|
||||
post.serial_process(RsGenericSerializer::SIZE_ESTIMATE,ctx);
|
||||
|
||||
if(ctx.mSize > 200000) {
|
||||
if(!ctx.mOk || ctx.mOffset > 200000) {
|
||||
error_message = "Maximum size of 200000 bytes exceeded for board post.";
|
||||
RsErr() << error_message;
|
||||
return false;
|
||||
}
|
||||
|
||||
return createPost(post,postId);
|
||||
if(!createPost(post,postId))
|
||||
{
|
||||
error_message = "Failed to publish the post. Check your signing identity and board permissions.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool p3Posted::setPostPinned(const RsGxsGroupId& boardId,
|
||||
const RsGxsMessageId& postId, bool pinned,
|
||||
std::string& errorMessage)
|
||||
{
|
||||
RS_STACK_MUTEX(mPinUpdateMutex);
|
||||
errorMessage.clear();
|
||||
std::vector<RsPostedGroup> groups;
|
||||
if(boardId.isNull() || postId.isNull()
|
||||
|| !getBoardsInfo({boardId}, groups) || groups.size() != 1)
|
||||
{
|
||||
errorMessage = "Board or post not found.";
|
||||
return false;
|
||||
}
|
||||
auto& board = groups.front();
|
||||
if(!IS_GROUP_ADMIN(board.mMeta.mSubscribeFlags))
|
||||
{
|
||||
errorMessage = "Only a board administrator can pin posts.";
|
||||
return false;
|
||||
}
|
||||
|
||||
RsGxsMessageId originalId = postId;
|
||||
if(pinned)
|
||||
{
|
||||
std::vector<RsPostedPost> posts;
|
||||
std::vector<RsGxsComment> comments;
|
||||
std::vector<RsGxsVote> votes;
|
||||
if(!getBoardContent(boardId, {postId}, posts, comments, votes)
|
||||
|| posts.size() != 1)
|
||||
{
|
||||
errorMessage = "The post is not available locally.";
|
||||
return false;
|
||||
}
|
||||
originalId = posts.front().mMeta.mMsgId;
|
||||
}
|
||||
// Unpinning remains possible even after a post expires locally.
|
||||
if(pinned == (board.mPinnedPosts.ids.count(originalId) != 0)) return true;
|
||||
if(pinned) board.mPinnedPosts.ids.insert(originalId);
|
||||
else board.mPinnedPosts.ids.erase(originalId);
|
||||
if(!editBoard(board))
|
||||
{
|
||||
errorMessage = "Failed to update the board's pinned posts.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool p3Posted::createCommentV2(
|
||||
|
||||
@ -101,7 +101,12 @@ virtual void receiveHelperChanges(std::vector<RsGxsNotify*>& changes)
|
||||
const RsGxsId& authorId,
|
||||
const RsGxsImage& image,
|
||||
RsGxsMessageId& postId,
|
||||
std::string& error_message) override;
|
||||
std::string& error_message,
|
||||
const RsGxsMessageId& origPostId = RsGxsMessageId()) override;
|
||||
|
||||
bool setPostPinned(const RsGxsGroupId& boardId,
|
||||
const RsGxsMessageId& postId, bool pinned,
|
||||
std::string& errorMessage) override;
|
||||
|
||||
bool voteForPost(const RsGxsGroupId& boardId,
|
||||
const RsGxsMessageId& postMsgId,
|
||||
@ -195,6 +200,8 @@ virtual void receiveHelperChanges(std::vector<RsGxsNotify*>& changes)
|
||||
}
|
||||
|
||||
protected:
|
||||
bool service_requiresAdminSignature(const RsGxsMsgMetaData& meta) const override;
|
||||
|
||||
virtual void notifyChanges(std::vector<RsGxsNotify*>& changes) override
|
||||
{
|
||||
return p3PostBase::notifyChanges(changes);
|
||||
@ -202,6 +209,7 @@ protected:
|
||||
|
||||
|
||||
private:
|
||||
RsMutex mPinUpdateMutex{"Posted pin updates"};
|
||||
// private part of blocking API
|
||||
bool vote(const RsGxsVote& vote,RsGxsMessageId& voteId,std::string& errorMessage);
|
||||
};
|
||||
|
||||
53
src/services/postedversions.h
Normal file
53
src/services/postedversions.h
Normal file
@ -0,0 +1,53 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "retroshare/rsposted.h"
|
||||
#include <map>
|
||||
|
||||
namespace PostedVersions
|
||||
{
|
||||
/** Resolve already authenticated revisions without moving comments or votes
|
||||
* to a new thread. An orphan revision is hidden until its original arrives. */
|
||||
inline void resolve(std::vector<RsPostedPost>& posts)
|
||||
{
|
||||
using Key = RsGxsGrpMsgIdPair;
|
||||
std::map<Key, const RsPostedPost*> originals;
|
||||
std::map<Key, const RsPostedPost*> latest;
|
||||
for(const auto& post : posts)
|
||||
{
|
||||
const auto& meta = post.mMeta;
|
||||
if(!meta.mParentId.isNull() || meta.mMsgId.isNull()) continue;
|
||||
if(meta.mOrigMsgId.isNull() || meta.mOrigMsgId == meta.mMsgId)
|
||||
originals[{meta.mGroupId, meta.mMsgId}] = &post;
|
||||
else
|
||||
{
|
||||
auto& revision = latest[{meta.mGroupId, meta.mOrigMsgId}];
|
||||
if(!revision || revision->mMeta.mPublishTs < meta.mPublishTs
|
||||
|| (revision->mMeta.mPublishTs == meta.mPublishTs
|
||||
&& revision->mMeta.mMsgId < meta.mMsgId))
|
||||
revision = &post;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<RsPostedPost> resolved;
|
||||
resolved.reserve(originals.size());
|
||||
for(const auto& entry : originals)
|
||||
{
|
||||
auto post = *entry.second;
|
||||
post.mRevisionId = post.mMeta.mMsgId;
|
||||
const auto revision = latest.find(entry.first);
|
||||
if(revision != latest.end()
|
||||
&& revision->second->mMeta.mPublishTs >= post.mMeta.mPublishTs)
|
||||
{
|
||||
const auto& edit = *revision->second;
|
||||
post.mMeta.mMsgName = edit.mMeta.mMsgName;
|
||||
post.mLink = edit.mLink;
|
||||
post.mNotes = edit.mNotes;
|
||||
post.mImage = edit.mImage;
|
||||
post.mRevisionId = edit.mMeta.mMsgId;
|
||||
}
|
||||
resolved.push_back(std::move(post));
|
||||
}
|
||||
posts.swap(resolved);
|
||||
}
|
||||
}
|
||||
64
tests/posted/CMakeLists.txt
Normal file
64
tests/posted/CMakeLists.txt
Normal file
@ -0,0 +1,64 @@
|
||||
cmake_minimum_required(VERSION 3.18)
|
||||
project(posted_regression_tests LANGUAGES CXX)
|
||||
enable_testing()
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
get_filename_component(RS_ROOT "../.." ABSOLUTE)
|
||||
set(RAPIDJSON_INCLUDE_DIR "${RS_ROOT}/../supportlibs/rapidjson/include" CACHE PATH "RapidJSON headers")
|
||||
set(RNP_INCLUDE_DIR "${RS_ROOT}/../supportlibs/librnp/include" CACHE PATH "RNP headers")
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
add_library(posted_test_support STATIC
|
||||
${RS_ROOT}/src/gxs/gxssecurity.cc
|
||||
${RS_ROOT}/src/gxs/rsgxsdata.cc
|
||||
${RS_ROOT}/src/rsitems/rsnxsitems.cc
|
||||
${RS_ROOT}/src/services/p3gxscommon.cc
|
||||
${RS_ROOT}/src/rsitems/rsposteditems.cc
|
||||
${RS_ROOT}/src/rsitems/rsgxscommentitems.cc
|
||||
${RS_ROOT}/src/rsitems/rsgxsitems.cc
|
||||
${RS_ROOT}/src/serialiser/rsserializable.cc
|
||||
${RS_ROOT}/src/serialiser/rsbaseserial.cc
|
||||
${RS_ROOT}/src/serialiser/rsserial.cc
|
||||
${RS_ROOT}/src/serialiser/rsserializer.cc
|
||||
${RS_ROOT}/src/serialiser/rstypeserializer.cc
|
||||
${RS_ROOT}/src/serialiser/rstlvbase.cc
|
||||
${RS_ROOT}/src/serialiser/rstlvbinary.cc
|
||||
${RS_ROOT}/src/serialiser/rstlvimage.cc
|
||||
${RS_ROOT}/src/serialiser/rstlvitem.cc
|
||||
${RS_ROOT}/src/serialiser/rstlvkeys.cc
|
||||
${RS_ROOT}/src/util/rsdebug.cc
|
||||
${RS_ROOT}/src/util/rsjson.cc
|
||||
${RS_ROOT}/src/util/rsstacktrace.cc
|
||||
${RS_ROOT}/src/util/rsbase64.cc
|
||||
${RS_ROOT}/src/util/rsprint.cc
|
||||
${RS_ROOT}/src/util/rsthreads.cc
|
||||
${RS_ROOT}/src/util/rsrandom.cc
|
||||
${RS_ROOT}/src/util/rsdir.cc
|
||||
${RS_ROOT}/src/util/rstime.cc
|
||||
${RS_ROOT}/src/util/smallobject.cc)
|
||||
target_include_directories(posted_test_support PUBLIC ${RS_ROOT}/src ${RAPIDJSON_INCLUDE_DIR}
|
||||
${RNP_INCLUDE_DIR} ${CMAKE_CURRENT_BINARY_DIR})
|
||||
target_link_libraries(posted_test_support PUBLIC OpenSSL::Crypto)
|
||||
target_compile_options(posted_test_support PUBLIC
|
||||
-Wno-deprecated-declarations -Wno-inconsistent-missing-override -Wno-cpp)
|
||||
target_compile_options(posted_test_support PRIVATE -ffunction-sections -fdata-sections)
|
||||
|
||||
# RNP's public headers include this generated export header, even though
|
||||
# these tests use only RetroShare's OpenSSL message-signature implementation.
|
||||
include(GenerateExportHeader)
|
||||
generate_export_header(posted_test_support BASE_NAME rnp EXPORT_MACRO_NAME RNP_API
|
||||
EXPORT_FILE_NAME rnp/rnp_export.h STATIC_DEFINE RNP_STATIC INCLUDE_GUARD_NAME RNP_EXPORT)
|
||||
|
||||
add_library(posted_service_compile_check OBJECT
|
||||
${RS_ROOT}/src/services/p3posted.cc ${RS_ROOT}/src/gxs/rsgenexchange.cc)
|
||||
target_link_libraries(posted_service_compile_check PRIVATE posted_test_support)
|
||||
|
||||
add_executable(posted_tests posted_tests.cc)
|
||||
target_link_libraries(posted_tests PRIVATE posted_test_support)
|
||||
if(APPLE)
|
||||
target_link_options(posted_tests PRIVATE -Wl,-dead_strip)
|
||||
else()
|
||||
target_link_options(posted_tests PRIVATE -Wl,--gc-sections)
|
||||
endif()
|
||||
add_test(NAME posted_regressions COMMAND posted_tests)
|
||||
58
tests/posted/README.md
Normal file
58
tests/posted/README.md
Normal file
@ -0,0 +1,58 @@
|
||||
# Posted Regression Tests
|
||||
|
||||
These tests build the Posted item serializer, revision resolver and GXS admin
|
||||
signature implementation without Qt or a running RetroShare node. They also
|
||||
compile the Posted service and shared GXS exchange implementation.
|
||||
|
||||
From the RetroShare checkout, initialize the header dependencies and run:
|
||||
|
||||
```sh
|
||||
git submodule update --init supportlibs/rapidjson supportlibs/librnp
|
||||
cmake -S libretroshare/tests/posted -B build-posted-tests
|
||||
cmake --build build-posted-tests -j4
|
||||
ctest --test-dir build-posted-tests --output-on-failure
|
||||
```
|
||||
|
||||
OpenSSL development files and a C++17 compiler are required. Set
|
||||
`OPENSSL_ROOT_DIR`, `RAPIDJSON_INCLUDE_DIR` or `RNP_INCLUDE_DIR` if necessary.
|
||||
|
||||
Coverage includes legacy groups with and without images, pinned groups with and
|
||||
without images, truncated pin data, deterministic latest-revision selection,
|
||||
missing originals, group isolation, original thread state and image retention,
|
||||
valid admin signatures, absent signatures, tampered messages, publisher-only
|
||||
keys and incorrect admin keys.
|
||||
|
||||
## Behavior and Compatibility
|
||||
|
||||
`createPostV2` accepts an optional final `origPostId` argument, preserving existing
|
||||
call sites. Edits require both an owned signing identity and the board admin key.
|
||||
Revisions receive an additional admin signature that updated peers verify.
|
||||
Unsigned revisions in pre-existing local databases are excluded from post data.
|
||||
|
||||
The blocking content APIs return one post per original thread. Its title, body,
|
||||
link and image come from the newest authenticated revision. `mRevisionId`
|
||||
identifies that revision; `mMeta.mMsgId`, author, timestamp, read state, comments
|
||||
and votes remain associated with the original. Revisions without a locally
|
||||
available original are hidden until it arrives. Low-level token APIs still
|
||||
return individual authenticated versions.
|
||||
|
||||
Pins live in the admin-signed board group and refer to original message IDs.
|
||||
Old groups remain readable, and unpinned groups retain their old wire layout.
|
||||
Older clients do not implement pinning or revision display; use updated clients
|
||||
on both peers to verify these features.
|
||||
|
||||
## GUI Checks
|
||||
|
||||
After building the Qt GUI, use an admin node and a subscribed non-admin node:
|
||||
|
||||
1. Create posts containing text, links and static or animated images. Edit each
|
||||
field, remove an image, and edit a post a second time. Confirm only one post
|
||||
appears and existing comments, votes and links still work.
|
||||
2. Right-click a post while another post is selected. Confirm Edit Post and
|
||||
Pin Post affect the right-clicked post. Non-admins must not see those actions.
|
||||
3. Pin several posts, select New, Top and Hot, and use search and pagination.
|
||||
Matching pinned posts must precede matching unpinned posts in every sort.
|
||||
4. Unpin a post, restart both nodes and edit the board description. Confirm the
|
||||
pin list survives reload, synchronization and unrelated board edits.
|
||||
5. Cause a publish failure. Confirm the dialog preserves text and image data,
|
||||
displays an error, and allows retrying without duplicate submissions.
|
||||
212
tests/posted/posted_tests.cc
Normal file
212
tests/posted/posted_tests.cc
Normal file
@ -0,0 +1,212 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
#include "rsitems/rsposteditems.h"
|
||||
#include "services/postedversions.h"
|
||||
#include "gxs/gxssecurity.h"
|
||||
#include "gxs/rsgxsdata.h"
|
||||
#include "rsitems/rsnxsitems.h"
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
|
||||
#define CHECK(condition) do { if(!(condition)) { \
|
||||
std::cerr << __LINE__ << ": " << #condition << std::endl; std::exit(1); \
|
||||
} } while(false)
|
||||
|
||||
static RsGxsMessageId id(char c) { return RsGxsMessageId(std::string(40, c)); }
|
||||
|
||||
static std::vector<uint8_t> encode(RsGxsPostedGroupItem& item)
|
||||
{
|
||||
RsGenericSerializer::SerializeContext size;
|
||||
item.serial_process(RsGenericSerializer::SIZE_ESTIMATE, size);
|
||||
std::vector<uint8_t> bytes(size.mOffset);
|
||||
RsGenericSerializer::SerializeContext out(bytes.data(), bytes.size());
|
||||
item.serial_process(RsGenericSerializer::SERIALIZE, out);
|
||||
CHECK(out.mOk && out.mOffset == bytes.size());
|
||||
return bytes;
|
||||
}
|
||||
|
||||
static void serialization()
|
||||
{
|
||||
for(bool image : {false, true})
|
||||
for(bool pins : {false, true})
|
||||
{
|
||||
RsPostedGroup group;
|
||||
group.mDescription = "Board description";
|
||||
uint8_t data[] = {1, 2, 3, 4};
|
||||
if(image) group.mGroupImage.copy(data, sizeof(data));
|
||||
if(pins) group.mPinnedPosts.ids = {id('1'), id('2')};
|
||||
RsGxsPostedGroupItem item;
|
||||
CHECK(item.fromPostedGroup(group, false));
|
||||
const auto bytes = encode(item);
|
||||
if(!pins)
|
||||
CHECK(bytes.size() == GetTlvStringSize(group.mDescription)
|
||||
+ (image ? item.mGroupImage.TlvSize() : 0));
|
||||
|
||||
RsGxsPostedGroupItem decoded;
|
||||
RsGenericSerializer::SerializeContext in(
|
||||
const_cast<uint8_t*>(bytes.data()), bytes.size());
|
||||
decoded.serial_process(RsGenericSerializer::DESERIALIZE, in);
|
||||
CHECK(in.mOk && in.mOffset == bytes.size());
|
||||
RsPostedGroup restored;
|
||||
CHECK(decoded.toPostedGroup(restored, true));
|
||||
CHECK(restored.mDescription == group.mDescription);
|
||||
CHECK(restored.mPinnedPosts.ids == group.mPinnedPosts.ids);
|
||||
CHECK(restored.mGroupImage.mSize == group.mGroupImage.mSize);
|
||||
if(image) CHECK(!memcmp(restored.mGroupImage.mData, data, sizeof(data)));
|
||||
item.clear();
|
||||
CHECK(item.mPinnedPosts.ids.empty());
|
||||
}
|
||||
|
||||
RsGxsPostedGroupItem pinned;
|
||||
pinned.mPinnedPosts.ids.insert(id('1'));
|
||||
auto bytes = encode(pinned);
|
||||
bytes.pop_back();
|
||||
RsGxsPostedGroupItem truncated;
|
||||
RsGenericSerializer::SerializeContext in(bytes.data(), bytes.size());
|
||||
truncated.serial_process(RsGenericSerializer::DESERIALIZE, in);
|
||||
CHECK(!in.mOk);
|
||||
}
|
||||
|
||||
static void revisions()
|
||||
{
|
||||
RsPostedPost original;
|
||||
original.mMeta.mGroupId = RsGxsGroupId(std::string(32, '1'));
|
||||
original.mMeta.mMsgId = id('1');
|
||||
original.mMeta.mPublishTs = 100;
|
||||
original.mMeta.mAuthorId = RsGxsId(std::string(32, '1'));
|
||||
original.mMeta.mMsgName = "Original";
|
||||
original.mMeta.mMsgStatus = GXS_SERV::GXS_MSG_STATUS_VOTE_UP;
|
||||
original.mUpVotes = 9;
|
||||
original.mComments = 3;
|
||||
original.mHaveVoted = true;
|
||||
|
||||
auto edit = original;
|
||||
edit.mMeta.mMsgId = id('2');
|
||||
edit.mMeta.mOrigMsgId = original.mMeta.mMsgId;
|
||||
edit.mMeta.mAuthorId = RsGxsId(std::string(32, '2'));
|
||||
edit.mMeta.mPublishTs = 101;
|
||||
edit.mMeta.mMsgName = "Edited";
|
||||
edit.mNotes = "New notes";
|
||||
edit.mLink = "https://example.org/edited";
|
||||
uint8_t data[] = {5, 6, 7};
|
||||
edit.mImage.copy(data, sizeof(data));
|
||||
edit.mUpVotes = edit.mComments = 0;
|
||||
|
||||
auto newest = edit;
|
||||
newest.mMeta.mMsgId = id('3');
|
||||
newest.mMeta.mMsgName = "Newest";
|
||||
// A deterministic tie-break is required when edits share a timestamp.
|
||||
std::vector<RsPostedPost> input = {original, edit, newest};
|
||||
do
|
||||
{
|
||||
auto posts = input;
|
||||
PostedVersions::resolve(posts);
|
||||
CHECK(posts.size() == 1);
|
||||
const auto& post = posts.front();
|
||||
CHECK(post.mMeta.mMsgId == original.mMeta.mMsgId);
|
||||
CHECK(post.mMeta.mAuthorId == original.mMeta.mAuthorId);
|
||||
CHECK(post.mMeta.mPublishTs == original.mMeta.mPublishTs);
|
||||
CHECK(post.mMeta.mMsgStatus == original.mMeta.mMsgStatus);
|
||||
CHECK(post.mRevisionId == newest.mMeta.mMsgId);
|
||||
CHECK(post.mMeta.mMsgName == "Newest");
|
||||
CHECK(post.mNotes == edit.mNotes && post.mLink == edit.mLink);
|
||||
CHECK(post.mImage.mSize == sizeof(data));
|
||||
CHECK(!memcmp(post.mImage.mData, data, sizeof(data)));
|
||||
CHECK(post.mUpVotes == 9 && post.mComments == 3 && post.mHaveVoted);
|
||||
} while(std::next_permutation(input.begin(), input.end(),
|
||||
[](const RsPostedPost& a, const RsPostedPost& b)
|
||||
{ return a.mMeta.mMsgId < b.mMeta.mMsgId; }));
|
||||
|
||||
std::vector<RsPostedPost> orphan = {edit};
|
||||
PostedVersions::resolve(orphan);
|
||||
CHECK(orphan.empty());
|
||||
edit.mMeta.mParentId = id('4');
|
||||
orphan = {original, edit};
|
||||
PostedVersions::resolve(orphan);
|
||||
CHECK(orphan.size() == 1 && orphan.front().mMeta.mMsgName == "Original");
|
||||
|
||||
edit.mMeta.mParentId.clear();
|
||||
edit.mMeta.mGroupId = RsGxsGroupId(std::string(32, '2'));
|
||||
orphan = {original, edit};
|
||||
PostedVersions::resolve(orphan);
|
||||
CHECK(orphan.size() == 1 && orphan.front().mMeta.mMsgName == "Original");
|
||||
|
||||
original.mMeta.mOrigMsgId = original.mMeta.mMsgId;
|
||||
orphan = {original};
|
||||
PostedVersions::resolve(orphan);
|
||||
CHECK(orphan.size() == 1);
|
||||
}
|
||||
|
||||
static void adminSignatures()
|
||||
{
|
||||
RsTlvPublicRSAKey publicKey;
|
||||
RsTlvPrivateRSAKey privateKey;
|
||||
CHECK(GxsSecurity::generateKeyPair(publicKey, privateKey));
|
||||
publicKey.keyFlags |= RSTLV_KEY_DISTRIB_ADMIN;
|
||||
privateKey.keyFlags |= RSTLV_KEY_DISTRIB_ADMIN;
|
||||
RsTlvSecurityKeySet keys;
|
||||
keys.public_keys[publicKey.keyId] = publicKey;
|
||||
keys.private_keys[privateKey.keyId] = privateKey;
|
||||
|
||||
RsNxsMsg msg(RS_SERVICE_GXS_TYPE_POSTED);
|
||||
msg.metaData = new RsGxsMsgMetaData;
|
||||
msg.metaData->mGroupId = RsGxsGroupId(std::string(32, '1'));
|
||||
msg.metaData->mOrigMsgId = id('1');
|
||||
msg.metaData->mAuthorId = RsGxsId(std::string(32, '2'));
|
||||
msg.metaData->mMsgName = "Admin edit";
|
||||
msg.metaData->mPublishTs = time(nullptr);
|
||||
const char payload[] = "Edited body";
|
||||
msg.msg.setBinData(payload, sizeof(payload));
|
||||
|
||||
uint32_t metaSize = msg.metaData->serial_size();
|
||||
std::vector<char> data(sizeof(payload) + metaSize);
|
||||
memcpy(data.data(), payload, sizeof(payload));
|
||||
CHECK(msg.metaData->serialise(data.data() + sizeof(payload), &metaSize));
|
||||
RsTlvKeySignature signature;
|
||||
CHECK(GxsSecurity::getAdminSignature(data.data(), data.size(), keys, signature));
|
||||
CHECK(!GxsSecurity::validateAdminSignature(msg, keys));
|
||||
msg.metaData->signSet.keySignSet[GxsSecurity::ADMIN_SIGNATURE_INDEX] = signature;
|
||||
msg.metaData->mMsgId = id('3');
|
||||
CHECK(GxsSecurity::validateAdminSignature(msg, keys));
|
||||
// Validation must preserve metadata and be repeatable.
|
||||
CHECK(GxsSecurity::validateAdminSignature(msg, keys));
|
||||
CHECK(msg.metaData->mMsgId == id('3') && msg.metaData->mOrigMsgId == id('1'));
|
||||
|
||||
msg.metaData->mMsgName = "Tampered";
|
||||
CHECK(!GxsSecurity::validateAdminSignature(msg, keys));
|
||||
msg.metaData->mMsgName = "Admin edit";
|
||||
msg.metaData->mOrigMsgId = id('4');
|
||||
CHECK(!GxsSecurity::validateAdminSignature(msg, keys));
|
||||
msg.metaData->mOrigMsgId = id('1');
|
||||
msg.metaData->mAuthorId = RsGxsId(std::string(32, '3'));
|
||||
CHECK(!GxsSecurity::validateAdminSignature(msg, keys));
|
||||
msg.metaData->mAuthorId = RsGxsId(std::string(32, '2'));
|
||||
static_cast<char*>(msg.msg.bin_data)[0] ^= 1;
|
||||
CHECK(!GxsSecurity::validateAdminSignature(msg, keys));
|
||||
static_cast<char*>(msg.msg.bin_data)[0] ^= 1;
|
||||
|
||||
auto publisherKeys = keys;
|
||||
publisherKeys.private_keys.begin()->second.keyFlags &= ~RSTLV_KEY_DISTRIB_ADMIN;
|
||||
publisherKeys.private_keys.begin()->second.keyFlags |= RSTLV_KEY_DISTRIB_PUBLISH;
|
||||
publisherKeys.public_keys.begin()->second.keyFlags &= ~RSTLV_KEY_DISTRIB_ADMIN;
|
||||
publisherKeys.public_keys.begin()->second.keyFlags |= RSTLV_KEY_DISTRIB_PUBLISH;
|
||||
CHECK(!GxsSecurity::getAdminSignature(data.data(), data.size(), publisherKeys, signature));
|
||||
CHECK(!GxsSecurity::validateAdminSignature(msg, publisherKeys));
|
||||
keys.private_keys.clear();
|
||||
CHECK(!GxsSecurity::getAdminSignature(data.data(), data.size(), keys, signature));
|
||||
CHECK(GxsSecurity::validateAdminSignature(msg, keys));
|
||||
|
||||
CHECK(GxsSecurity::generateKeyPair(publicKey, privateKey));
|
||||
publicKey.keyFlags |= RSTLV_KEY_DISTRIB_ADMIN;
|
||||
keys.public_keys.clear();
|
||||
keys.public_keys[publicKey.keyId] = publicKey;
|
||||
CHECK(!GxsSecurity::validateAdminSignature(msg, keys));
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
serialization();
|
||||
revisions();
|
||||
adminSignatures();
|
||||
std::cout << "Posted serialization, revision and admin-signature regressions passed" << std::endl;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user