Policy
Core to Cordial Treasury is the sophisticated policy engine. There is a general access policy and a dedicated second stage transfer policy, each consisting of multiple rules (the access rules and the transfer rules). The technical reference is in the API documentation (AccessRuleData and TransferRuleData), here we will focus on the general principles, what it means for policy to "pass", and our considerations in designing the policy engine.
Request Flow
As a user of a Cordial Treasury, you interact by sending signed requests to the Treasury API. Each such request intends to cause a CRUD (create, read, update, or delete) operation on the state of the Treasury, which itself consists of the totality of resources. There is currently no authentication nor authorization for read operations, the following applies only to mutations (create, update, delete, and custom actions). The flow can be summarized as follows:
- user sends HTTP request to API, with JSON payload, signed with a signature for one of their credentials
- engine verifies the signature on the request, and uses it to identify the user: authentication
- such a request can initiate an operation, or approve or cancel an existing operation
- an operation has an operation ID, which is a hash "over the entire request" by the initiator
- access policy is applied to determine if the user is allowed for the request (authorization of the user)
- depending on the operation, policy may require a quorum of approvals (authorization of the operation)
- if the request is to create a transfer, transfer policy additionally applies
- once an authorized quorum of users have authorized an operation, the engine executes it
Since policy applies to every mutation (create, update, delete, and custom actions), the immediate response to a request is
- either a direct "deny" (if the user is not authorized), or
- an operation ID
In the second case, a special Operation resource is created.
Operations may be listed via GET api-url:v1/operations
, and a specific operation may be retrieved via
GET api-url:v1/operations/<operation-id>
. They store the entire initiating request,
and track the participants of the operation (the initiator, the approvers, and potentially a canceler).
An operation is always in a certain state, which may be: authorizing
, creating-resource
, succeeded
, or failed
.
Rules
Policy is default deny - every operation must have at least one rule allowing it.
Besides allow
rules, there are also require
rules (additional requirements),
and explicit deny
rules. The entire policy at a given point in time consists of
the three lists of active access and transfer rules of these three rule variants.
Any rule has two or three sections, each of which are a filter.
- The applicable filter (the "what"). For an access rule, these are: action, resource, and data filter. For a transfer rule, these are: asset, amount, from, and to filter. They serve to scope out requests/operations to which the rule applies, if it does, we call the rule applicable to the request.
- The appropriate filter (the "who"). These are: initiate, approve, and cancel filter. They serve to scope out which users may participate in requests the rule applies to, if they do, we call the rule appropriate (as in appropriate behavior of the users). So appropriate rules are a subset of applicable rules (for a given request).
- For allow and require rules, the quorum filter. This is currently just the approvals filter. It serves to require a quorum of users approving a filter (which may be just one, or may be more). There is currently no "cancels filter"; a cancellation - if authorized - is immediate. We call an appropriate rule quorum (for a given request).
Policy Evaluation
The input is always a request, which itself comes in three variants: initiate
if the user
does not reference any previous operations, and approve
or cancel
, which both must reference
an existing operation. Policy decisions occur after the authentication stage.
Tentatively, an operation is constructed, either from scratch (when initiating) or by updating the approvers/cancelers data (when approving/canceling). The access policy (and for transfers in a subsequent stage the transfer policy) is applied to this tentative next operation state.
As a first step, all non-applicable rules are discarded.
In the following three substages, denial causes the tentative operation change to be reverted. In case the request is not denied, then the changes to the operation become permanent.
Denial
At this stage, requests may be immediately rejected:
- if no appropriate allow rule exists, "default deny"
- if an inappropriate require rule exists, "explicit (require) deny"
- if an appropriate deny rule exists, "explicit deny"
In any of these cases, changes to the operation are reversed.
Denial at this stage means that while some other user constellation might perform the operation (there may be applicable rules), but they are not the right ones (the allows and requires are not appropriate to the users, or some deny rule is appropriate and explicitly rules out these users).
Cancellation
At this stage, the involved users are authorized for the request.
If there exists a cancel participant, the operation is set to failed, which is a terminal state.
In particular, this change to the operation will be stored in engine state.
Decision
Decisions are made purely in terms of the remaining appropriate allow and require rules. Note that inappropriate require rules no longer exist (they would have caused an early deny), while some applicable allow rules may not be appropriate - they are disregarded for decision making.
There are two cases:
-
If there exists at least one quorum allow rule and all require rules are quorum, then the operation is
authorized
. Except for create transfer operations - in which case the same procedure is followed to check transfer policy. -
Otherwise - if no allow rule or not all require rules (all of which appropriate) have reached quorum - the operation is in
authorizing
state. It is ready for other users to approve or cancel.
For an authorized
operation, if it is a create operation, there may be
an intermediate creating-resource
state, but in any case the operation is performed by the engine,
and will eventually have succeeded
or failed
. In these two terminal states of an operation,
the operation is immutable, approve or cancel requests relating to it will be denied.
Some Examples
We will use Cordial Configuration Language (CCL) in this section, which hopefully is somewhat self-explaining.
allow access rule { action = "create", resource = "Account", initiate = "any/user" }
This rule allows "any user" to create an Account, without additional requirements.
By itself, any create account request will immediately be authorized
.
require access rule { action = "create", resource = "Account",
approve = "roles/manager", approvals = 1 }
By adding this rule to the previous, existing permissions of users to create accounts are restricted.
Any initiating create account request will result in an authorizing
operation, which may be
canceled by any user, but only an approving request by a user with role "manager" will cause the operation
to be authorized
, which will immediately result in a new account being created by the engine, and the
operation will have succeeded
.