ZaStoGram_desktop/Telegram/SourceFiles/e2e_cloud/group/group_state.cpp
2026-08-01 07:51:49 +03:00

471 lines
15 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "e2e_cloud/group/group_state.h"
#include "e2e_cloud/group/group_transition_codec.h"
#include <algorithm>
#include <limits>
#include <set>
#include <utility>
namespace E2ECloud {
std::optional<ProtectedGroupState> ProtectedGroupState::Create(
CreateGroupStateArgs args) {
if (!args.conversationId
|| !args.ownerAccountId
|| !args.ownerClientId
|| !args.ownerTelegramUserIdBinding
|| !IsValidHistoryAccess(args.policy.defaultHistoryAccess)) {
return std::nullopt;
}
auto result = ProtectedGroupState();
result._conversationId = args.conversationId;
result._generation = 1;
result._policy = args.policy;
result._members.push_back({
.accountId = args.ownerAccountId,
.telegramUserIdBinding = args.ownerTelegramUserIdBinding,
.role = GroupRole::Owner,
.adminPermissions = 0,
.historyAccess = {
.mode = HistoryAccessMode::Full,
.boundaryEventId = {},
},
.joinedGeneration = result._generation,
.clients = { args.ownerClientId },
});
return result;
}
std::optional<ProtectedGroupState> ProtectedGroupState::Restore(
ProtectedGroupStateSnapshot snapshot) {
if (!snapshot.conversationId
|| !snapshot.generation
|| !IsValidHistoryAccess(snapshot.policy.defaultHistoryAccess)
|| snapshot.members.empty()
|| snapshot.appliedTransitionIds.size()
!= snapshot.generation - 1) {
return std::nullopt;
}
auto accountIds = std::set<AccountId>();
auto telegramUserIds = std::set<std::uint64_t>();
auto clientIds = std::set<ClientId>();
auto ownerCount = 0;
for (const auto &member : snapshot.members) {
if (!member.accountId
|| !member.telegramUserIdBinding
|| !member.joinedGeneration
|| member.joinedGeneration > snapshot.generation
|| member.clients.empty()
|| !IsValidHistoryAccess(member.historyAccess)
|| !accountIds.emplace(member.accountId).second
|| !telegramUserIds.emplace(
member.telegramUserIdBinding).second
|| (member.adminPermissions & ~kAllAdminPermissions)) {
return std::nullopt;
}
switch (member.role) {
case GroupRole::Member:
if (member.adminPermissions) {
return std::nullopt;
}
break;
case GroupRole::Administrator:
break;
case GroupRole::Owner:
if (member.adminPermissions) {
return std::nullopt;
}
++ownerCount;
break;
default:
return std::nullopt;
}
for (const auto &clientId : member.clients) {
if (!clientId || !clientIds.emplace(clientId).second) {
return std::nullopt;
}
}
}
if (ownerCount != 1) {
return std::nullopt;
}
auto transitionIds = std::set<ObjectId>();
for (const auto &transitionId : snapshot.appliedTransitionIds) {
if (!transitionId || !transitionIds.emplace(transitionId).second) {
return std::nullopt;
}
}
if ((snapshot.generation == 1
&& (snapshot.lastTransitionId
|| !transitionIds.empty()))
|| (snapshot.generation > 1
&& (!snapshot.lastTransitionId
|| !transitionIds.contains(snapshot.lastTransitionId)))) {
return std::nullopt;
}
auto result = ProtectedGroupState();
result._conversationId = snapshot.conversationId;
result._generation = snapshot.generation;
result._lastTransitionId = snapshot.lastTransitionId;
result._policy = snapshot.policy;
result._members = std::move(snapshot.members);
result._appliedTransitionIds = std::move(transitionIds);
return result;
}
GroupTransitionResult ProtectedGroupState::validate(
const GroupTransition &transition,
const GroupTransitionAuthentication &authentication) const {
if (!IsValidGroupTransitionStructure(transition)) {
return GroupTransitionResult::InvalidStructure;
} else if (transition.conversationId != _conversationId) {
return GroupTransitionResult::WrongConversation;
} else if (!transition.transitionId
|| _appliedTransitionIds.contains(transition.transitionId)) {
return GroupTransitionResult::InvalidTransitionId;
} else if (_generation == std::numeric_limits<std::uint64_t>::max()
|| transition.previousGeneration != _generation
|| transition.generation != _generation + 1) {
return GroupTransitionResult::InvalidGeneration;
}
const auto actor = member(authentication.actor.accountId);
if (!actor) {
return GroupTransitionResult::UnknownActor;
} else if (std::find(
begin(actor->clients),
end(actor->clients),
authentication.actor.clientId) == end(actor->clients)) {
return GroupTransitionResult::InvalidActorClient;
}
const auto target = member(transition.targetAccountId);
const auto credential = authentication.targetClientAuthorization;
switch (transition.kind) {
case GroupTransitionKind::AddMember:
if (!actorHasPermission(*actor, AdminPermission::ManageAdmissions)) {
return GroupTransitionResult::PermissionDenied;
} else if (!transition.targetAccountId
|| !transition.targetClientId
|| !transition.targetTelegramUserIdBinding) {
return GroupTransitionResult::InvalidTarget;
} else if (!credential
|| credential->accountId != transition.targetAccountId
|| credential->clientId != transition.targetClientId) {
return GroupTransitionResult::InvalidTargetCredential;
} else if (target) {
return GroupTransitionResult::TargetAlreadyMember;
} else if (memberByTelegramUserId(
transition.targetTelegramUserIdBinding)) {
return GroupTransitionResult::TelegramUserAlreadyBound;
} else if (memberByClient(transition.targetClientId)) {
return GroupTransitionResult::ClientAlreadyExists;
} else if (!IsValidHistoryAccess(transition.historyAccess)) {
return GroupTransitionResult::InvalidHistoryAccess;
} else if (!actorMayGrant(*actor, transition.historyAccess)) {
return GroupTransitionResult::PermissionDenied;
}
return GroupTransitionResult::Allowed;
case GroupTransitionKind::RemoveMember:
if (!actorHasPermission(*actor, AdminPermission::RemoveMembers)) {
return GroupTransitionResult::PermissionDenied;
} else if (!target) {
return GroupTransitionResult::TargetNotMember;
} else if (target->role == GroupRole::Owner) {
return GroupTransitionResult::OwnerInvariant;
} else if (target->role == GroupRole::Administrator
&& actor->role != GroupRole::Owner) {
return GroupTransitionResult::PermissionDenied;
}
return GroupTransitionResult::Allowed;
case GroupTransitionKind::AddClient:
if (!target) {
return GroupTransitionResult::TargetNotMember;
} else if (!transition.targetClientId) {
return GroupTransitionResult::InvalidTarget;
} else if (!credential
|| credential->accountId != transition.targetAccountId
|| credential->clientId != transition.targetClientId) {
return GroupTransitionResult::InvalidTargetCredential;
} else if (memberByClient(transition.targetClientId)) {
return GroupTransitionResult::ClientAlreadyExists;
}
return GroupTransitionResult::Allowed;
case GroupTransitionKind::RemoveClient:
if (!target) {
return GroupTransitionResult::TargetNotMember;
} else if (!transition.targetClientId) {
return GroupTransitionResult::InvalidTarget;
} else if (actor->accountId != transition.targetAccountId
&& !actorHasPermission(
*actor,
AdminPermission::RemoveMembers)) {
return GroupTransitionResult::PermissionDenied;
} else if (std::find(
begin(target->clients),
end(target->clients),
transition.targetClientId) == end(target->clients)) {
return GroupTransitionResult::ClientNotFound;
} else if (target->clients.size() == 1) {
return GroupTransitionResult::InvalidTarget;
}
return GroupTransitionResult::Allowed;
case GroupTransitionKind::SetRole:
if (actor->role != GroupRole::Owner) {
return GroupTransitionResult::PermissionDenied;
} else if (!target) {
return GroupTransitionResult::TargetNotMember;
} else if (target->role == GroupRole::Owner) {
return GroupTransitionResult::OwnerInvariant;
} else if (transition.targetRole != GroupRole::Member
&& transition.targetRole != GroupRole::Administrator) {
return GroupTransitionResult::InvalidRole;
} else if (!validPermissions(transition.targetAdminPermissions)
|| (transition.targetRole == GroupRole::Member
&& transition.targetAdminPermissions)) {
return GroupTransitionResult::InvalidPermissions;
}
return GroupTransitionResult::Allowed;
case GroupTransitionKind::TransferOwnership:
if (actor->role != GroupRole::Owner) {
return GroupTransitionResult::PermissionDenied;
} else if (!target) {
return GroupTransitionResult::TargetNotMember;
} else if (target->role == GroupRole::Owner) {
return GroupTransitionResult::OwnerInvariant;
}
return GroupTransitionResult::Allowed;
case GroupTransitionKind::SetDefaultHistory:
if (!actorHasPermission(
*actor,
AdminPermission::ChangeDefaultHistory)) {
return GroupTransitionResult::PermissionDenied;
} else if (!IsValidHistoryAccess(transition.historyAccess)) {
return GroupTransitionResult::InvalidHistoryAccess;
} else if (!actorMayGrant(*actor, transition.historyAccess)) {
return GroupTransitionResult::PermissionDenied;
}
return GroupTransitionResult::Allowed;
case GroupTransitionKind::SetMemberHistory:
if (!target) {
return GroupTransitionResult::TargetNotMember;
} else if (!IsValidHistoryAccess(transition.historyAccess)) {
return GroupTransitionResult::InvalidHistoryAccess;
} else if (!actorMayGrant(*actor, transition.historyAccess)) {
return GroupTransitionResult::PermissionDenied;
}
return GroupTransitionResult::Allowed;
}
return GroupTransitionResult::InvalidTarget;
}
GroupTransitionResult ProtectedGroupState::applyVerified(
const GroupTransition &transition,
const GroupTransitionAuthentication &authentication) {
const auto validation = validate(transition, authentication);
if (validation != GroupTransitionResult::Allowed) {
return validation;
}
const auto actorAccountId = authentication.actor.accountId;
switch (transition.kind) {
case GroupTransitionKind::AddMember:
_members.push_back({
.accountId = transition.targetAccountId,
.telegramUserIdBinding = transition.targetTelegramUserIdBinding,
.role = GroupRole::Member,
.adminPermissions = 0,
.historyAccess = transition.historyAccess,
.joinedGeneration = transition.generation,
.clients = { transition.targetClientId },
});
break;
case GroupTransitionKind::RemoveMember:
_members.erase(std::remove_if(
begin(_members),
end(_members),
[&](const GroupMember &member) {
return member.accountId == transition.targetAccountId;
}), end(_members));
break;
case GroupTransitionKind::AddClient:
memberMutable(transition.targetAccountId)->clients.push_back(
transition.targetClientId);
break;
case GroupTransitionKind::RemoveClient: {
auto &clients = memberMutable(transition.targetAccountId)->clients;
clients.erase(std::remove(
begin(clients),
end(clients),
transition.targetClientId), end(clients));
break;
}
case GroupTransitionKind::SetRole: {
auto target = memberMutable(transition.targetAccountId);
target->role = transition.targetRole;
target->adminPermissions = transition.targetAdminPermissions;
break;
}
case GroupTransitionKind::TransferOwnership: {
auto previous = memberMutable(actorAccountId);
auto target = memberMutable(transition.targetAccountId);
previous->role = GroupRole::Administrator;
previous->adminPermissions = kDefaultAdminPermissions;
target->role = GroupRole::Owner;
target->adminPermissions = 0;
break;
}
case GroupTransitionKind::SetDefaultHistory:
_policy.defaultHistoryAccess = transition.historyAccess;
break;
case GroupTransitionKind::SetMemberHistory:
memberMutable(transition.targetAccountId)->historyAccess =
transition.historyAccess;
break;
}
_generation = transition.generation;
_lastTransitionId = transition.transitionId;
_appliedTransitionIds.emplace(transition.transitionId);
return GroupTransitionResult::Allowed;
}
bool ProtectedGroupState::applyVerifiedForkRecovery(
ObjectId recoveryId,
std::uint64_t resolvedGeneration) {
if (!recoveryId
|| resolvedGeneration != _generation
|| _generation == std::numeric_limits<std::uint64_t>::max()
|| _appliedTransitionIds.contains(recoveryId)) {
return false;
}
++_generation;
_lastTransitionId = recoveryId;
_appliedTransitionIds.emplace(recoveryId);
return true;
}
ConversationId ProtectedGroupState::conversationId() const {
return _conversationId;
}
std::uint64_t ProtectedGroupState::generation() const {
return _generation;
}
ObjectId ProtectedGroupState::lastTransitionId() const {
return _lastTransitionId;
}
const GroupPolicy &ProtectedGroupState::policy() const {
return _policy;
}
const std::vector<GroupMember> &ProtectedGroupState::members() const {
return _members;
}
const GroupMember *ProtectedGroupState::member(AccountId accountId) const {
const auto i = std::find_if(
begin(_members),
end(_members),
[&](const GroupMember &member) {
return member.accountId == accountId;
});
return (i != end(_members)) ? &*i : nullptr;
}
const GroupMember *ProtectedGroupState::memberByTelegramUserId(
std::uint64_t telegramUserId) const {
const auto i = std::find_if(
begin(_members),
end(_members),
[&](const GroupMember &member) {
return member.telegramUserIdBinding == telegramUserId;
});
return (i != end(_members)) ? &*i : nullptr;
}
const GroupMember *ProtectedGroupState::memberByClient(ClientId clientId) const {
const auto i = std::find_if(
begin(_members),
end(_members),
[&](const GroupMember &member) {
return std::find(
begin(member.clients),
end(member.clients),
clientId) != end(member.clients);
});
return (i != end(_members)) ? &*i : nullptr;
}
ProtectedGroupStateSnapshot ProtectedGroupState::snapshot() const {
return {
.conversationId = _conversationId,
.generation = _generation,
.lastTransitionId = _lastTransitionId,
.policy = _policy,
.members = _members,
.appliedTransitionIds = {
_appliedTransitionIds.begin(),
_appliedTransitionIds.end(),
},
};
}
GroupMember *ProtectedGroupState::memberMutable(AccountId accountId) {
const auto i = std::find_if(
begin(_members),
end(_members),
[&](const GroupMember &member) {
return member.accountId == accountId;
});
return (i != end(_members)) ? &*i : nullptr;
}
bool ProtectedGroupState::actorHasPermission(
const GroupMember &actor,
AdminPermission permission) const {
return actor.role == GroupRole::Owner
|| (actor.role == GroupRole::Administrator
&& (actor.adminPermissions & PermissionMask(permission)));
}
bool ProtectedGroupState::actorMayGrant(
const GroupMember &actor,
const HistoryAccess &access) const {
if (!actorHasPermission(actor, AdminPermission::GrantHistory)) {
return false;
}
return access.mode != HistoryAccessMode::Full
|| actorHasPermission(actor, AdminPermission::GrantFullHistory);
}
bool ProtectedGroupState::validPermissions(
AdminPermissions permissions) const {
return (permissions & ~kAllAdminPermissions) == 0;
}
LocalGroupTransitionAuthorizer::LocalGroupTransitionAuthorizer(
const ProtectedGroupState &state,
const FreshnessGate &freshness)
: _state(state)
, _freshness(freshness) {
}
GroupTransitionResult LocalGroupTransitionAuthorizer::validate(
const GroupTransition &transition,
const GroupTransitionAuthentication &authentication) const {
if (!_freshness.administrationAllowed()
|| _freshness.knownCheckpoint().conversationId
!= _state.conversationId()) {
return GroupTransitionResult::FreshnessRequired;
}
return _state.validate(transition, authentication);
}
} // namespace E2ECloud