Skip to content

Commit

Permalink
CompareAndSetAccountDetail command
Browse files Browse the repository at this point in the history
Signed-off-by: artyom-yurin <[email protected]>
  • Loading branch information
ortyomka authored and lebdron committed Jul 24, 2019
1 parent c2a814e commit c85ec93
Show file tree
Hide file tree
Showing 23 changed files with 753 additions and 3 deletions.
60 changes: 60 additions & 0 deletions docs/source/api/commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -834,3 +834,63 @@ Possible Stateful Validation Errors

.. [#f1] https://www.ietf.org/rfc/rfc1035.txt
.. [#f2] https://www.ietf.org/rfc/rfc1123.txt
Compare and Set Account Detail
------------------------------

Purpose
^^^^^^^

Purpose of compare and set account detail command is to set key-value information for a given account if the old value matches the value passed.

Schema
^^^^^^

.. code-block:: proto
message CompareAndSetAccountDetail{
string account_id = 1;
string key = 2;
string value = 3;
oneof opt_old_value {
string old_value = 4;
}
}
.. note::
Pay attention, that old_value field is optional.
This is due to the fact that the key-value pair might not exist.

Structure
^^^^^^^^^

.. csv-table::
:header: "Field", "Description", "Constraint", "Example"
:widths: 15, 30, 20, 15

"Account ID", "id of the account to which the key-value information was set. If key-value pair doesnot exist , then it will be created", "an existing account", "artyom@soramitsu"
"Key", "key of information being set", "`[A-Za-z0-9_]{1,64}`", "Name"
"Value", "new value for the corresponding key", "length of value ≤ 4096", "Artyom"
"Old value", "current value for the corresponding key", "length of value ≤ 4096", "Artem"

Validation
^^^^^^^^^^

Three cases:

Case 1. When transaction creator wants to set account detail to his/her account and he or she has permission GetMyAccountDetail / GetAllAccountsDetail / GetDomainAccountDetail

Case 2. When transaction creator wants to set account detail to another account and he or she has permissions SetAccountDetail and GetAllAccountsDetail / GetDomainAccountDetail

Case 3. SetAccountDetail permission was granted to transaction creator and he or she has permission GetAllAccountsDetail / GetDomainAccountDetail

Possible Stateful Validation Errors
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. csv-table::
:header: "Code", "Error Name", "Description", "How to solve"

"1", "Could not compare and set account detail", "Internal error happened", "Try again or contact developers"
"2", "No such permissions", "Command's creator does not have permission to set and read account detail for this account", "Grant the necessary permission"
"3", "No such account", "Cannot find account to set account detail to", "Make sure account id is correct"
"4", "No match values", "Old values do not match", "Make sure old value is correct"
5 changes: 5 additions & 0 deletions irohad/ametsuchi/command_executor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ namespace shared_model {
class SetQuorum;
class SubtractAssetQuantity;
class TransferAsset;
class CompareAndSetAccountDetail;
} // namespace interface
} // namespace shared_model

Expand Down Expand Up @@ -119,6 +120,10 @@ namespace iroha {

virtual CommandResult operator()(
const shared_model::interface::TransferAsset &command) = 0;

virtual CommandResult operator()(
const shared_model::interface::CompareAndSetAccountDetail
&command) = 0;
};
} // namespace ametsuchi
} // namespace iroha
Expand Down
187 changes: 187 additions & 0 deletions irohad/ametsuchi/impl/postgres_command_executor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "interfaces/commands/add_peer.hpp"
#include "interfaces/commands/add_signatory.hpp"
#include "interfaces/commands/append_role.hpp"
#include "interfaces/commands/compare_and_set_account_detail.hpp"
#include "interfaces/commands/create_account.hpp"
#include "interfaces/commands/create_asset.hpp"
#include "interfaces/commands/create_domain.hpp"
Expand Down Expand Up @@ -266,6 +267,71 @@ namespace {
return query;
}

shared_model::interface::types::DomainIdType getDomainFromName(
const shared_model::interface::types::AccountIdType &account_id) {
// TODO 03.10.18 andrei: IR-1728 Move getDomainFromName to shared_model
std::vector<std::string> res;
boost::split(res, account_id, boost::is_any_of("@"));
return res.at(1);
}

/**
* Generate an SQL subquery which checks if creator has corresponding
* permissions for target account
* It verifies individual, domain, and global permissions, and returns true if
* any of listed permissions is present
*/
auto hasQueryPermission(
const shared_model::interface::types::AccountIdType &creator,
const shared_model::interface::types::AccountIdType &target_account,
shared_model::interface::permissions::Role indiv_permission_id,
shared_model::interface::permissions::Role all_permission_id,
shared_model::interface::permissions::Role domain_permission_id,
const shared_model::interface::types::DomainIdType &creator_domain,
const shared_model::interface::types::DomainIdType
&target_account_domain) {
const auto bits = shared_model::interface::RolePermissionSet::size();
const auto perm_str =
shared_model::interface::RolePermissionSet({indiv_permission_id})
.toBitstring();
const auto all_perm_str =
shared_model::interface::RolePermissionSet({all_permission_id})
.toBitstring();
const auto domain_perm_str =
shared_model::interface::RolePermissionSet({domain_permission_id})
.toBitstring();

boost::format cmd(R"(
has_indiv_perm AS (
SELECT (COALESCE(bit_or(rp.permission), '0'::bit(%1%))
& '%3%') = '%3%' FROM role_has_permissions AS rp
JOIN account_has_roles AS ar on ar.role_id = rp.role_id
WHERE ar.account_id = %2%
),
has_all_perm AS (
SELECT (COALESCE(bit_or(rp.permission), '0'::bit(%1%))
& '%4%') = '%4%' FROM role_has_permissions AS rp
JOIN account_has_roles AS ar on ar.role_id = rp.role_id
WHERE ar.account_id = %2%
),
has_domain_perm AS (
SELECT (COALESCE(bit_or(rp.permission), '0'::bit(%1%))
& '%5%') = '%5%' FROM role_has_permissions AS rp
JOIN account_has_roles AS ar on ar.role_id = rp.role_id
WHERE ar.account_id = %2%
),
has_query_perm AS (
SELECT (%2% = %6% AND (SELECT * FROM has_indiv_perm))
OR (SELECT * FROM has_all_perm)
OR (%7% = %8% AND (SELECT * FROM has_domain_perm)) AS perm
)
)");

return (cmd % bits % creator % perm_str % all_perm_str % domain_perm_str
% target_account % creator_domain % target_account_domain)
.str();
}

std::string checkAccountDomainRoleOrGlobalRolePermission(
shared_model::interface::permissions::Role global_permission,
shared_model::interface::permissions::Role domain_permission,
Expand Down Expand Up @@ -797,6 +863,51 @@ namespace iroha {
ELSE 1
END AS result)";

const std::string PostgresCommandExecutor::compareAndSetAccountDetailBase =
R"(PREPARE %s (text, text, text, text, text, text, text) AS
WITH %s
old_value AS
(
SELECT *
FROM account
WHERE
account_id = $2
AND CASE
WHEN data ? $1 AND data->$1 ?$3
THEN
CASE
WHEN $5 IS NOT NULL THEN data->$1->$3 = $5::jsonb
ELSE FALSE
END
ELSE TRUE
END
),
inserted AS
(
UPDATE account
SET data = jsonb_set(
CASE
WHEN data ? $1 THEN data
ELSE jsonb_set(data, array[$1], '{}')
END,
array[$1, $3], $4::jsonb
)
WHERE
EXISTS (SELECT * FROM old_value)
AND account_id = $2
%s
RETURNING (1)
)
SELECT CASE
WHEN EXISTS (SELECT * FROM inserted) THEN 0
WHEN NOT EXISTS
(SELECT * FROM account WHERE account_id=$2) THEN 3
WHEN NOT EXISTS (SELECT * FROM old_value) THEN 4
%s
ELSE 1
END
AS result)";

std::string CommandError::toString() const {
return (boost::format("%s: %d with extra info '%s'") % command_name
% error_code % error_extra)
Expand Down Expand Up @@ -1228,6 +1339,43 @@ namespace iroha {
sql_, cmd.str(), "TransferAsset", std::move(str_args));
}

CommandResult PostgresCommandExecutor::operator()(
const shared_model::interface::CompareAndSetAccountDetail &command) {
auto &account_id = command.accountId();
auto &key = command.key();
auto &value = command.value();
auto &old_value = command.oldValue();

auto cmd = boost::format(
"EXECUTE %1% ('%2%', '%3%', '%4%', '%5%', %6%, '%7%', '%8%')");

appendCommandName("compareAndSetAccountDetail", cmd, do_validation_);

std::string new_json_value = "\"" + value + "\"";
std::string expected_json_value = "NULL";

if (old_value) {
expected_json_value = "'\"" + old_value.get() + "\"'";
}

cmd = (cmd % creator_account_id_ % account_id % key % new_json_value
% expected_json_value % getDomainFromName(creator_account_id_)
% getDomainFromName(account_id));

auto str_args = [&account_id, &key, &new_json_value, &old_value] {
return getQueryArgsStringBuilder()
.append("account_id", account_id)
.append("key", key)
.append("value", new_json_value)
.append("old_value",
old_value ? "\"" + old_value.get() + "\"" : "NULL")
.finalize();
};

return executeQuery(
sql_, cmd.str(), "compareAndSetAccountDetail", std::move(str_args));
}

void PostgresCommandExecutor::prepareStatements(soci::session &sql) {
std::vector<PreparedStatement> statements;

Expand Down Expand Up @@ -1598,6 +1746,45 @@ namespace iroha {
R"( AND (SELECT * FROM has_perm))",
R"( WHEN NOT (SELECT * FROM has_perm) THEN 2 )"}});

statements.push_back(
{"compareAndSetAccountDetail",
compareAndSetAccountDetailBase,
{(boost::format(R"(
has_role_perm AS (%s),
has_grantable_perm AS (%s),
%s,
has_perm AS (SELECT CASE
WHEN (SELECT * FROM has_query_perm) THEN
CASE
WHEN (SELECT * FROM has_grantable_perm)
THEN true
WHEN ($1 = $2) THEN true
WHEN (SELECT * FROM has_role_perm)
THEN true
ELSE false END
ELSE false END
),
)")
% checkAccountRolePermission(
shared_model::interface::permissions::Role::kSetDetail, "$1")
% checkAccountGrantablePermission(
shared_model::interface::permissions::Grantable::
kSetMyAccountDetail,
"$1",
"$2")
% hasQueryPermission(
"$1",
"$2",
shared_model::interface::permissions::Role::kGetMyAccDetail,
shared_model::interface::permissions::Role::kGetAllAccDetail,
shared_model::interface::permissions::Role::
kGetDomainAccDetail,
"$6",
"$7"))
.str(),
R"( AND (SELECT * FROM has_perm))",
R"( WHEN NOT (SELECT * FROM has_perm) THEN 2 )"}});

for (const auto &st : statements) {
prepareStatement(sql, st);
}
Expand Down
5 changes: 5 additions & 0 deletions irohad/ametsuchi/impl/postgres_command_executor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ namespace iroha {
CommandResult operator()(
const shared_model::interface::TransferAsset &command) override;

CommandResult operator()(
const shared_model::interface::CompareAndSetAccountDetail &command)
override;

static void prepareStatements(soci::session &sql);

private:
Expand Down Expand Up @@ -111,6 +115,7 @@ namespace iroha {
static const std::string setQuorumBase;
static const std::string subtractAssetQuantityBase;
static const std::string transferAssetBase;
static const std::string compareAndSetAccountDetailBase;
};
} // namespace ametsuchi
} // namespace iroha
Expand Down
1 change: 1 addition & 0 deletions shared_model/backend/protobuf/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ add_library(shared_model_proto_backend
commands/impl/proto_set_quorum.cpp
commands/impl/proto_subtract_asset_quantity.cpp
commands/impl/proto_transfer_asset.cpp
commands/impl/proto_compare_and_set_account_detail.cpp
queries/impl/proto_query.cpp
queries/impl/proto_get_account.cpp
queries/impl/proto_get_account_asset_transactions.cpp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "backend/protobuf/commands/proto_add_peer.hpp"
#include "backend/protobuf/commands/proto_add_signatory.hpp"
#include "backend/protobuf/commands/proto_append_role.hpp"
#include "backend/protobuf/commands/proto_compare_and_set_account_detail.hpp"
#include "backend/protobuf/commands/proto_create_account.hpp"
#include "backend/protobuf/commands/proto_create_asset.hpp"
#include "backend/protobuf/commands/proto_create_domain.hpp"
Expand Down Expand Up @@ -43,7 +44,8 @@ namespace {
shared_model::proto::SetQuorum,
shared_model::proto::SubtractAssetQuantity,
shared_model::proto::TransferAsset,
shared_model::proto::RemovePeer>;
shared_model::proto::RemovePeer,
shared_model::proto::CompareAndSetAccountDetail>;

/// list of types in proto variant
using ProtoCommandListType = ProtoCommandVariantType::types;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Copyright Soramitsu Co., Ltd. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

#include "backend/protobuf/commands/proto_compare_and_set_account_detail.hpp"

namespace shared_model {
namespace proto {

CompareAndSetAccountDetail::CompareAndSetAccountDetail(
iroha::protocol::Command &command)
: compare_and_set_account_detail_{
command.compare_and_set_account_detail()} {}

const interface::types::AccountIdType &
CompareAndSetAccountDetail::accountId() const {
return compare_and_set_account_detail_.account_id();
}

const interface::types::AccountDetailKeyType &
CompareAndSetAccountDetail::key() const {
return compare_and_set_account_detail_.key();
}

const interface::types::AccountDetailValueType &
CompareAndSetAccountDetail::value() const {
return compare_and_set_account_detail_.value();
}

const boost::optional<interface::types::AccountDetailValueType>
CompareAndSetAccountDetail::oldValue() const {
if (compare_and_set_account_detail_.opt_old_value_case()
== iroha::protocol::CompareAndSetAccountDetail::
OPT_OLD_VALUE_NOT_SET) {
return boost::none;
}
return compare_and_set_account_detail_.old_value();
}

} // namespace proto
} // namespace shared_model
Loading

0 comments on commit c85ec93

Please sign in to comment.