变更记录

  • 2019-11-06:初始草案
  • 2020-10-12:更新草案
  • 2020-11-13:已接受
  • 2020-05-06:proto API 更新,使用 sdk.Msg 替代 sdk.ServiceMsg(后者概念已从 Cosmos SDK 中移除)
  • 2022-04-20:更新 SendAuthorization 的 proto 文档,以明确 SpendLimit 是必填字段。(可使用通用授权配合 bank msg type url 创建无额度限制的 bank 授权)

状态

已接受

摘要

本 ADR 定义了 x/authz 模块,该模块允许账户向其他账户授予授权,使其能够代表该账户执行操作。

背景

推动该模块设计的具体用例包括:
  • 希望将提案投票能力委托给除已委托质押所对应账户之外的其他账户
  • 最初在 #4480 中提出的“sub-keys”功能,这是一个术语,用来描述本模块与 ADR 029 中的 fee_grant 模块以及 group module 共同提供的功能。
“sub-keys”功能大致指一个账户能够将其部分能力授予其他账户,而这些其他账户可以使用较弱但更易用的安全措施。例如,一个代表组织的主账户可以授予单个员工账户支出少量组织资金的能力。或者,一个个人(或群体)使用多签钱包时,可以将提案投票能力授予任意一个成员密钥。 当前实现基于 Gaian 团队在 2019 年柏林 Hackatom 上完成的工作。

决策

我们将创建一个名为 authz 的模块,用于提供将任意权限从一个账户(granter,授权方)授予另一个账户(grantee,被授权方)的能力。授权必须针对特定的 Msg 服务方法逐一授予,并通过 Authorization 接口的实现来完成。

类型

授权决定了具体授予哪些权限。它们具有可扩展性,可以为任意 Msg 服务方法定义,甚至可以定义在声明该 Msg 方法的模块之外。Authorization 通过 TypeURL 引用 Msg。

Authorization

type Authorization interface {
    proto.Message

	// MsgTypeURL 返回完整限定的 Msg TypeURL(见 ADR 020),
	// 该 TypeURL 将处理请求并决定接受或拒绝。
	MsgTypeURL()

string

	// Accept 判断此授权是否允许执行给定的 sdk.Msg,
	// 如果允许,则返回升级后的授权实例。
	Accept(ctx sdk.Context, msg sdk.Msg) (AcceptResponse, error)

	// ValidateBasic 执行一个简单的校验,
	// 不需要访问任何其他信息。
	ValidateBasic()

error
}

// AcceptResponse 用于告知 authz 消息控制器该请求是否被接受,
// 以及授权是否应被更新或删除。
type AcceptResponse struct {
	// 如果 Accept=true,控制器可以接受该授权并处理更新。
	Accept bool
	// 如果 Delete=true,控制器必须删除授权对象并释放
	// 存储资源。
	Delete bool
	// 调用 Authorization.Accept 的控制器必须检查 `Updated != nil`。如果为真,
	// 则必须使用更新后的版本,并在存储层处理更新。
	Updated Authorization
}
例如,可以为 MsgSend 定义如下的 SendAuthorization,它接收一个 SpendLimit,并在使用后将其递减直到归零:
type SendAuthorization struct {
	// SpendLimit 指定此授权最多可花费的代币数量,
	// 并会随着代币支出而更新。此字段为必填项。(可使用通用授权
	// 配合 bank msg type url 创建无额度限制的 bank 授权)。
	SpendLimit sdk.Coins
}

func (a SendAuthorization)

MsgTypeURL()

string {
    return sdk.MsgTypeURL(&MsgSend{
})
}

func (a SendAuthorization)

Accept(ctx sdk.Context, msg sdk.Msg) (authz.AcceptResponse, error) {
    mSend, ok := msg.(*MsgSend)
    if !ok {
    return authz.AcceptResponse{
}, sdkerrors.ErrInvalidType.Wrap("类型不匹配")
}

limitLeft, isNegative := a.SpendLimit.SafeSub(mSend.Amount)
    if isNegative {
    return authz.AcceptResponse{
}, sdkerrors.ErrInsufficientFunds.Wrapf("请求金额超过支出上限")
}
    if limitLeft.IsZero() {
    return authz.AcceptResponse{
    Accept: true,
    Delete: true
}, nil
}

return authz.AcceptResponse{
    Accept: true,
    Delete: false,
    Updated: &SendAuthorization{
    SpendLimit: limitLeft
}}, nil
}
还可以基于 Authorization 接口为 MsgSend 实现另一种能力类型,而无需修改底层 bank 模块。
关于 AcceptResponse 的几点说明
  • 如果授权被接受,AcceptResponse.Accept 字段将被设为 true。 但如果被拒绝,Accept 函数会直接返回错误(不会将 AcceptResponse.Accept 设为 false)。
  • 只有当授权确实发生变化时,AcceptResponse.Updated 字段才会被设为非 nil 值。 如果授权保持不变(例如在 GenericAuthorization 中通常总是如此),该字段将为 nil。

Msg 服务

service Msg {
  // Grant 将给定授权以指定过期时间授予 grantee,作用于 granter 的
  // 账户。
  rpc Grant(MsgGrant) returns (MsgGrantResponse);

  // Exec 尝试使用授予 grantee 的授权来执行提供的消息。
  // 每条消息应当只有一个签名者,并且该签名者应对应授权的 granter。
  rpc Exec(MsgExec) returns (MsgExecResponse);

  // Revoke 撤销 granter 账户授予 grantee 的、与给定方法名对应的
  // 任意授权。
  rpc Revoke(MsgRevoke) returns (MsgRevokeResponse);
}

// Grant 赋予在指定过期时间内执行
// 所提供方法的权限。
message Grant {
  google.protobuf.Any       authorization = 1 [(cosmos_proto.accepts_interface) = "cosmos.authz.v1beta1.Authorization"];
  google.protobuf.Timestamp expiration    = 2 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false];
}

message MsgGrant {
  string granter = 1;
  string grantee = 2;

  Grant grant = 3 [(gogoproto.nullable) = false];
}

message MsgExecResponse {
  cosmos.base.abci.v1beta1.Result result = 1;
}

message MsgExec {
  string   grantee                  = 1;
  // 需要执行的授权消息。每个 msg 都必须实现 Authorization 接口
  repeated google.protobuf.Any msgs = 2 [(cosmos_proto.accepts_interface) = "cosmos.base.v1beta1.Msg"];;
}

路由中间件

authz 的 Keeper 将暴露一个 DispatchActions 方法,使其他模块能够基于 Authorization 授权将 Msg 发送到路由器:
type Keeper interface {
	// 如果 grantee 已获得授权,可以通过每个 msg 的第一个(也是唯一一个)
	// 签名者发送这些消息,则 DispatchActions 会将提供的 msgs 路由到各自的处理器。
    DispatchActions(ctx sdk.Context, grantee sdk.AccAddress, msgs []sdk.Msg)

sdk.Result`
}

CLI

tx exec 方法

当 CLI 用户希望使用 MsgExec 代表其他账户运行交易时,可以使用 exec 方法。例如,gaiacli tx gov vote 1 yes --from <grantee> --generate-only | gaiacli tx authz exec --send-as <granter> --from <grantee> 会发送如下交易:
MsgExec {
    Grantee: mykey,
    Msgs: []sdk.Msg{
    MsgVote {
    ProposalID: 1,
    Voter: cosmos3thsdgh983egh823
      Option: Yes
}
 
}
}

tx grant <grantee> <authorization> --from <granter>

该 CLI 命令会发送一笔 MsgGrant 交易。authorization 应在 CLI 中编码为 JSON。

tx revoke <grantee> <method-name> --from <granter>

该 CLI 命令会发送一笔 MsgRevoke 交易。

内置授权

SendAuthorization

// SendAuthorization 允许 grantee 从 granter 的账户中
// 最多支出 spend_limit 数量的代币。
message SendAuthorization {
  repeated cosmos.base.v1beta1.Coin spend_limit = 1;
}

GenericAuthorization

// GenericAuthorization 赋予 grantee 不受限制的权限,
// 可代表 granter 的账户执行所提供的方法。
message GenericAuthorization {
  option (cosmos_proto.implements_interface) = "Authorization";

  // 通过 type URL 标识的 Msg,用于授予不受限制的执行权限
  string msg = 1;
}

影响

正面

  • 用户将能够授权其他用户代表其账户执行任意操作,从而改善许多场景下的密钥管理
  • 该方案比先前考虑的方法更通用,并且基于 Authorization 接口的方法可以由 SDK 用户扩展,以覆盖其他用例

负面

中性

参考

  • 初始 Hackatom 实现:Link
  • Hackatom 之后的规范:Link
  • B-Harvest subkeys 规范:Link

Changelog

  • 2019-11-06: Initial Draft
  • 2020-10-12: Updated Draft
  • 2020-11-13: Accepted
  • 2020-05-06: proto API updates, use sdk.Msg instead of sdk.ServiceMsg (the latter concept was removed from Cosmos SDK)
  • 2022-04-20: Updated the SendAuthorization proto docs to clarify the SpendLimit is a required field. (Generic authorization can be used with bank msg type url to create limit less bank authorization)

Status

Accepted

Abstract

This ADR defines the x/authz module which allows accounts to grant authorizations to perform actions on behalf of that account to other accounts.

Context

The concrete use cases which motivated this module include:
  • the desire to delegate the ability to vote on proposals to other accounts besides the account which one has delegated stake
  • “sub-keys” functionality, as originally proposed in #4480 which is a term used to describe the functionality provided by this module together with the fee_grant module from ADR 029 and the group module.
The “sub-keys” functionality roughly refers to the ability for one account to grant some subset of its capabilities to other accounts with possibly less robust, but easier to use security measures. For instance, a master account representing an organization could grant the ability to spend small amounts of the organization’s funds to individual employee accounts. Or an individual (or group) with a multisig wallet could grant the ability to vote on proposals to any one of the member keys. The current implementation is based on work done by the Gaian’s team at Hackatom Berlin 2019.

Decision

We will create a module named authz which provides functionality for granting arbitrary privileges from one account (the granter) to another account (the grantee). Authorizations must be granted for a particular Msg service methods one by one using an implementation of Authorization interface.

Types

Authorizations determine exactly what privileges are granted. They are extensible and can be defined for any Msg service method even outside of the module where the Msg method is defined. Authorizations reference Msgs using their TypeURL.

Authorization

type Authorization interface {
    proto.Message

	// MsgTypeURL returns the fully-qualified Msg TypeURL (as described in ADR 020),
	// which will process and accept or reject a request.
	MsgTypeURL()

string

	// Accept determines whether this grant permits the provided sdk.Msg to be performed, and if
	// so provides an upgraded authorization instance.
	Accept(ctx sdk.Context, msg sdk.Msg) (AcceptResponse, error)

	// ValidateBasic does a simple validation check that
	// doesn't require access to any other information.
	ValidateBasic()

error
}

// AcceptResponse instruments the controller of an authz message if the request is accepted
// and if it should be updated or deleted.
type AcceptResponse struct {
	// If Accept=true, the controller can accept and authorization and handle the update.
	Accept bool
	// If Delete=true, the controller must delete the authorization object and release
	// storage resources.
	Delete bool
	// Controller, who is calling Authorization.Accept must check if `Updated != nil`. If yes,
	// it must use the updated version and handle the update on the storage level.
	Updated Authorization
}
For example a SendAuthorization like this is defined for MsgSend that takes a SpendLimit and updates it down to zero:
type SendAuthorization struct {
	// SpendLimit specifies the maximum amount of tokens that can be spent
	// by this authorization and will be updated as tokens are spent. This field is required. (Generic authorization 
	// can be used with bank msg type url to create limit less bank authorization).
	SpendLimit sdk.Coins
}

func (a SendAuthorization)

MsgTypeURL()

string {
    return sdk.MsgTypeURL(&MsgSend{
})
}

func (a SendAuthorization)

Accept(ctx sdk.Context, msg sdk.Msg) (authz.AcceptResponse, error) {
    mSend, ok := msg.(*MsgSend)
    if !ok {
    return authz.AcceptResponse{
}, sdkerrors.ErrInvalidType.Wrap("type mismatch")
}

limitLeft, isNegative := a.SpendLimit.SafeSub(mSend.Amount)
    if isNegative {
    return authz.AcceptResponse{
}, sdkerrors.ErrInsufficientFunds.Wrapf("requested amount is more than spend limit")
}
    if limitLeft.IsZero() {
    return authz.AcceptResponse{
    Accept: true,
    Delete: true
}, nil
}

return authz.AcceptResponse{
    Accept: true,
    Delete: false,
    Updated: &SendAuthorization{
    SpendLimit: limitLeft
}}, nil
}
A different type of capability for MsgSend could be implemented using the Authorization interface with no need to change the underlying bank module.
Small notes on AcceptResponse
  • The AcceptResponse.Accept field will be set to true if the authorization is accepted. However, if it is rejected, the function Accept will raise an error (without setting AcceptResponse.Accept to false).
  • The AcceptResponse.Updated field will be set to a non-nil value only if there is a real change to the authorization. If authorization remains the same (as is, for instance, always the case for a GenericAuthorization), the field will be nil.

Msg Service

service Msg {
  // Grant grants the provided authorization to the grantee on the granter's
  // account with the provided expiration time.
  rpc Grant(MsgGrant) returns (MsgGrantResponse);

  // Exec attempts to execute the provided messages using
  // authorizations granted to the grantee. Each message should have only
  // one signer corresponding to the granter of the authorization.
  rpc Exec(MsgExec) returns (MsgExecResponse);

  // Revoke revokes any authorization corresponding to the provided method name on the
  // granter's account that has been granted to the grantee.
  rpc Revoke(MsgRevoke) returns (MsgRevokeResponse);
}

// Grant gives permissions to execute
// the provided method with expiration time.
message Grant {
  google.protobuf.Any       authorization = 1 [(cosmos_proto.accepts_interface) = "cosmos.authz.v1beta1.Authorization"];
  google.protobuf.Timestamp expiration    = 2 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false];
}

message MsgGrant {
  string granter = 1;
  string grantee = 2;

  Grant grant = 3 [(gogoproto.nullable) = false];
}

message MsgExecResponse {
  cosmos.base.abci.v1beta1.Result result = 1;
}

message MsgExec {
  string   grantee                  = 1;
  // Authorization Msg requests to execute. Each msg must implement Authorization interface
  repeated google.protobuf.Any msgs = 2 [(cosmos_proto.accepts_interface) = "cosmos.base.v1beta1.Msg"];;
}

Router Middleware

The authz Keeper will expose a DispatchActions method which allows other modules to send Msgs to the router based on Authorization grants:
type Keeper interface {
	// DispatchActions routes the provided msgs to their respective handlers if the grantee was granted an authorization
	// to send those messages by the first (and only)

signer of each msg.
    DispatchActions(ctx sdk.Context, grantee sdk.AccAddress, msgs []sdk.Msg)

sdk.Result`
}

CLI

tx exec Method

When a CLI user wants to run a transaction on behalf of another account using MsgExec, they can use the exec method. For instance gaiacli tx gov vote 1 yes --from <grantee> --generate-only | gaiacli tx authz exec --send-as <granter> --from <grantee> would send a transaction like this:
MsgExec {
    Grantee: mykey,
    Msgs: []sdk.Msg{
    MsgVote {
    ProposalID: 1,
    Voter: cosmos3thsdgh983egh823
      Option: Yes
}
 
}
}

tx grant <grantee> <authorization> --from <granter>

This CLI command will send a MsgGrant transaction. authorization should be encoded as JSON on the CLI.

tx revoke <grantee> <method-name> --from <granter>

This CLI command will send a MsgRevoke transaction.

Built-in Authorizations

SendAuthorization

// SendAuthorization allows the grantee to spend up to spend_limit coins from
// the granter's account.
message SendAuthorization {
  repeated cosmos.base.v1beta1.Coin spend_limit = 1;
}

GenericAuthorization

// GenericAuthorization gives the grantee unrestricted permissions to execute
// the provided method on behalf of the granter's account.
message GenericAuthorization {
  option (cosmos_proto.implements_interface) = "Authorization";

  // Msg, identified by it's type URL, to grant unrestricted permissions to execute
  string msg = 1;
}

Consequences

Positive

  • Users will be able to authorize arbitrary actions on behalf of their accounts to other users, improving key management for many use cases
  • The solution is more generic than previously considered approaches and the Authorization interface approach can be extended to cover other use cases by SDK users

Negative

Neutral

References

  • Initial Hackatom implementation: Link
  • Post-Hackatom spec: Link
  • B-Harvest subkeys spec: Link