Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FLIP: Capability Controllers #798

Merged
merged 45 commits into from
May 12, 2023
Merged
Changes from 3 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
7fad23d
Capability Controllers
janezpodhostnik Feb 3, 2022
25107db
Update flip number
janezpodhostnik Feb 3, 2022
3a2fe24
Apply suggestions from code review
pgebheim May 17, 2022
8e9d625
rename authaccount to issuer
janezpodhostnik Aug 11, 2022
94e62d4
minor text changes
janezpodhostnik Aug 11, 2022
b141357
Capabilities are values
janezpodhostnik Aug 22, 2022
8f08496
More examples and some API changes
janezpodhostnik Sep 26, 2022
08afdd7
remove some not relevant text
janezpodhostnik Oct 11, 2022
84f4422
update CapCons
janezpodhostnik Oct 18, 2022
5254aad
fix b14135748e0cac3b80920a235b200d6da8785ee9
turbolent Oct 24, 2022
6a66339
Apply suggestions from code review
turbolent Nov 9, 2022
b358e18
cap cons changes
janezpodhostnik Nov 15, 2022
c336d65
get returns optional
janezpodhostnik Nov 17, 2022
08c5696
fix typo
turbolent Nov 23, 2022
f11550e
capcons capability ids
janezpodhostnik Nov 25, 2022
4530d47
Apply suggestions from code review
turbolent Jan 24, 2023
1b1abe1
improve formatting
turbolent Jan 24, 2023
0a705fb
improve comments
turbolent Jan 24, 2023
a34c3f5
fix uses of Capability's ID field
turbolent Jan 24, 2023
fe89f85
use docstring syntax
turbolent Jan 28, 2023
2a468fc
remove CapabilityController.getCapability, maybe add later if needed
turbolent Feb 2, 2023
fd39cb5
clarify comment of retarget function
turbolent Feb 2, 2023
b5b0417
return references to capability controllers instead of owned types
turbolent Feb 2, 2023
d3bfc4a
capability IDs are not UUIDs
turbolent Feb 8, 2023
5611f6a
Replace revoke with delete
turbolent Feb 14, 2023
1974801
new Account.capabilities.borrow function needs type bound
turbolent Feb 16, 2023
dfb4111
clarify behaviour of AuthAccount.Capabilities.borrow
turbolent Feb 16, 2023
635b061
integrate account capabilities
turbolent Mar 14, 2023
0bd9d6b
remove issue height field
turbolent Mar 14, 2023
4dc251c
describe migration of existing data
turbolent Mar 14, 2023
51fa67b
make AccountCapabilities.issue future-proof
turbolent Mar 24, 2023
8099abe
update front matter
turbolent Mar 24, 2023
6cdbe3c
fix docstrings and examples
turbolent Apr 4, 2023
10229d6
add publishing/unpublishing functions, revert get to getCapability
turbolent Apr 13, 2023
9ae0fb1
refactor capability related functionality back into a common nested t…
turbolent Apr 14, 2023
27fc82f
reorder AuthAccount.Capabilities members
turbolent Apr 14, 2023
968ca78
improve StorageCapabilityController.retarget function
turbolent Apr 20, 2023
509b485
remove argument label for function parameter of AuthAccount.StorageCa…
turbolent Apr 21, 2023
eb76d8a
improve type bound of AuthAccount.AccountCapabilities.issue, make sup…
turbolent Apr 28, 2023
9a4f57d
update deployment and migration
turbolent May 4, 2023
0c34ed4
clarify storability of controllers
turbolent May 4, 2023
a1ed443
add tag field to controllers, replace pet names section with one for …
turbolent May 4, 2023
ac73832
Merge branch 'master' into janez/capability-controllers
turbolent May 10, 2023
3ee30df
clarify behaviour of controller iteration
turbolent May 11, 2023
8dba94c
Accept FLIP
turbolent May 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
341 changes: 341 additions & 0 deletions flips/20220203-capability-controllers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,341 @@
# Capability controllers

| Status | Proposed |
| :------------ | :------------------------------------------------- |
| **FLIP #** | [798](https://github.com/onflow/flow/pull/798) |
| **Author(s)** | Janez Podhostnik (janez.podhostnik@dapperlabs.com) |
| **Updated** | 2022-02-03 |

## Preface

Cadence encourages a [capability-based security](https://en.wikipedia.org/wiki/Capability-based_security) model as described on the Flow [doc site](https://docs.onflow.org/cadence/language/capability-based-access-control). Capabilities are themselves a new concept that most Cadence programmers need to understand, but the API for syntax around Capabilities (especially the notion of “links” and “linking”), and the associated concepts of the public and private storage domains, lead to Capabilities being even more confusing and awkward to use. This proposal suggests that we could get rid of links entirely, and replace them with Capability Controllers (henceforth referred to as CapCons) which could make Capabilities easier to understand, and easier to work with.

The following is a quick refresher of the current state of the Capabilities API (from the [flow doc site](https://docs.onflow.org/cadence/language/capability-based-access-control)).

### Example Resource definition

In the following examples let's assume that the following interface and resource type that implements the interface are defined.

```cadence
pub resource interface HasCount {
pub count: Int
}

pub resource Counter: HasCount {
pub var count: Int

pub init(count: Int) {
self.count = count
}
}
```

The _issuer_ (`AuthAccount`) has also created an instance of this resource and saved it in its private storage.

```cadence
AuthAccount.save(<-create Counter(count: 42), to: /storage/counter)
```
janezpodhostnik marked this conversation as resolved.
Show resolved Hide resolved

### Public Capabilities

To allow anyone (read) access to the `count` field on the counter, the _issuer_ needs to create a public typed link at a chosen path that points to their stored counter resource.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not 100% accurate. You can use getAuthAccount in a script and borrow this from storate and read the value without having a public capability.

You cannot do so in transactions though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think eventually we need to introduce similar functionality to transactions too


```cadence
AuthAccount.link<&{HasCount}>(/public/hasCount, target: /storage/counter)
```
janezpodhostnik marked this conversation as resolved.
Show resolved Hide resolved

Anyone can now read the `count` of the counter resource via the `HasCount` interface. This can be done by

- getting the `PublicAccount` object of the issuer (using the issuers address),
- then getting a typed capability from the chosen path,
- then finally calling borrow on that capability to get a reference to the instance of the counter (constrained by the `HasCount` interface)

```cadence
let publicAccount = getAccount(issuerAddress)
let countCap = publicAccount.getCapability<&{HasCount}>(/public/hasCount)
let countRef = countCap.borrow()!
countRef.count
```

### Private Capabilities

To allow only certain accounts/resources (read) access to the `count` field on the counter, the _issuer_ needs to create a private typed link at a chosen path that points to their stored counter resource.

```cadence
AuthAccount.link<&{HasCount}>(/private/hasCount, target: /storage/counter)
janezpodhostnik marked this conversation as resolved.
Show resolved Hide resolved
```

The receiving party needs to offer a public way to receive `&{HasCount}` capabilities.

```cadence
// this would probably be defined on the same contract as `HasCount`
pub resource interface HasCountReceiverPublic {
pub fun addCapability(cap: Capability<&{HasCount}>)
}

pub resource HasCountReceiver: HasCountReceiverPublic {

pub var hasCountCapability: Capability<&{HasCount}>?

init() {
self.hasCountCapability = nil
}

pub fun addCapability(cap: Capability<&{HasCount}>) {
self.hasCountCapability = cap
}
}

//...
// this is the receiver account setup
let hasCountReceiver <- HasCountReceiver()

receivingPartyAuthAccount.save(<-hasCountReceiver, to: /storage/hasCountReceiver)
receivingPartyAuthAccount.link<&{HasCountReceiverPublic}>(/public/hasCountReceiver,
janezpodhostnik marked this conversation as resolved.
Show resolved Hide resolved
target: /storage/hasCountReceive)
```

With this in place the _issuer_ can create a capability from its private link and send it to this receiver.

```cadence
let countCap = authAccount.getCapability<&{HasCount}>(/private/hasCount)
janezpodhostnik marked this conversation as resolved.
Show resolved Hide resolved

let publicAccount = getAccount(receivingPartyAddress)
let countReceiverCap = publicAccount.getCapability<&{HasCountReceiverPublic}>(/public/hasCountReceiver)
let countReceiverRef = countCap.borrow()!
countRef.addCapability(cap: countCap)
```

The receiving party can then use this capability when they wish.

### Capability API requirements

There are two requirements of the capability system that must be satisfied.

- **Revocation**: Any capability must be revocable by the issuer.
- **Redirection**: The issuer should have the ability to redirect the capability to a different (compatible) object.

Revocation can be currently done by using the `unlink` function.
turbolent marked this conversation as resolved.
Show resolved Hide resolved

In the public example this would mean calling `authAccount.unlink<&{HasCount}>(/public/hasCount)` which would invalidate (break) all capabilities created from this public path (both those created before unlink was called and those created after unlink was called).

In the private example the call would change to `authAccount.unlink<&{HasCount}>(/private/hasCount)`. It is important to note that if the issuer wants to have the ability to revoke/redirect capabilities in a more granular way (instead of doing them all at once), the solution is to create multiple private linked paths (e.g. `/private/hasCountAlice`, `/private/hasCountBob`).
janezpodhostnik marked this conversation as resolved.
Show resolved Hide resolved

If a path that was unlinked is linked again all the capabilities created from that path resume working. This can be used to redirect a capability to a different object, but can also be dangerous if done unintentionally, reviving a previously revoked capability.

### Capabilities are values

Capabilities are a value type. This means that any capability that an account has access to can be copied, and potentially given to someone else. The copied capability will use the link on the same path as the original capability

## Problem statement

There are two potential pain points in the current capability API.

The first one is that capabilities/linking/revoking/redirecting are hard to understand for new developers that are coming to Flow, as that is a lot of new concepts to grasp before one can get started.

The second is that issuing and managing private capabilities that can be revoked at a granular level, by creating a custom private linked path for each capability, is difficult.

- Unless the path name includes some hints, there is no way to know when a path was created, thus making it more difficult to remember why a certain capability was issued.
- If an old path (that has been unlinked) is accidentally reused and re-linked, this will revive a capability that is supposed to be revoked.
- Copying a capability is easy (since it is a value type) but doing so might give unintended access to third parties.

## Suggested change

The suggested change addresses these pain points by:

- Removing (or abstracting away) the concept of links.
- Making it easier to create new capabilities (with individual links).
- Making capabilities resources, so that it is more difficult to give unintended access to third parties.
- Offering a way to iterate through capabilities on a storage path.
- Removing the need to have a `/private/`domain.
turbolent marked this conversation as resolved.
Show resolved Hide resolved
- Changing the `/public/` domain to be able to store capabilities (and only capabilities).
- Introducing Capability Controllers (CapCons) that handle management of capabilities.

### Capabilities as Resources

Changing capabilities into resources attempts to address two problems.

The first problem is that, as resources, capabilities would not be able to be copied. While a reference to a capability can still be created and passed on, this is a more explicit process than just simply creating a duplicate of a capability.

The second problem is a revocation problem. With capabilities as values the following scenario can happen:

Alice created capabilities B and C and gave them to Bob and Charlie respectively. Bob then also copied his capability (marked as B’) and gave it to Dan. The picture now looks like this.

- Bob has capability B
- Charlie has capability C
- Dan has capability B’

Revoking C yields the expected result that Charlie can no longer use his capability. However, revoking B also revokes all copies of B, so both Bob’s and Dan’s capabilities are revoked. This is not very intuitive at first glance, as there is little differnce between the capabilities
B and C, but Dan’s ability to use his capability depends on which copy he has.

With capabilities as resources this scenario would not have occurred as there is no way to copy B to create B’. If Dan also needs this capability, Alice must create a capability D to give to him. However this also means that there is no way for Bob to directly grant this capability to someone else (without losing his own).

Dan could also have a reference to the capability B (&B), but in this case Dan knows that his capability is just a reference, and that if B is revoked it makes sense that his reference to B also stops working.

### Accounts public domain as a capability storage

This part of the change proposes that accounts can store capabilities in their public domain. Capabilities would be borrowed by anyone that needs to use them. The `borrowCapability `method would be added to the PublicAccount which would be equivalent to how we currently get the capability then call `borrow` on it.

```cadence
let publicAccount = getAccount(issuerAddress)
let countCap = publicAccount.borrowCapability<&{HasCount}>(/public/hasCount)!
countRef.count
```

### Capability Controllers (CapCons)

Each Capability would have an associated CapCon that would be created together with the Capability (Capabilities and Capability Controllers are in a 1 to 1 relation). The data associated with CapCons would be stored in arrays, so that each storage path on an account has an array of CapCons of Capabilities issued from that storage path.
janezpodhostnik marked this conversation as resolved.
Show resolved Hide resolved

CapCons are a non-storable object.
joshuahannan marked this conversation as resolved.
Show resolved Hide resolved

Capabilities also have an address field and a target field. The target field is the storage path of the targeted object. Creating a capability from a capability should be illegal.
janezpodhostnik marked this conversation as resolved.
Show resolved Hide resolved

The definition of the `CapabilityController` .

```cadence
// CapabilityController can be retrieved via:
// - authAccount.getControllers(path: StoragePath): [CapabilityController]
// - authAccount.getController(capabilityId: UInt64): CapabilityController?
janezpodhostnik marked this conversation as resolved.
Show resolved Hide resolved
struct CapabilityController {
// The block height when the capability was created.
let issueHeight: UInt64
// The id of the related capability
// This is the UUiD of the created capability
janezpodhostnik marked this conversation as resolved.
Show resolved Hide resolved
let capabilityId: UInt64
janezpodhostnik marked this conversation as resolved.
Show resolved Hide resolved

// Is the capability revoked.
fun isRevoked(): Bool
turbolent marked this conversation as resolved.
Show resolved Hide resolved
// The target storage path.
fun target(): StoragePath
// Revoke the capability.
// Returns true if successfully revoked.
fun revoke(): Bool
// Restore the capability.
// Returns true if successfully restored.
fun restore(): Bool
// Retarget the capability.
// Returns if successfully restored.
// This would move the CapCon from one CapCon array to another
joshuahannan marked this conversation as resolved.
Show resolved Hide resolved
fun retarget(target: StoragePath): Bool
}
turbolent marked this conversation as resolved.
Show resolved Hide resolved
```

The auth account would get new methods to get CapCons in order to manage capabilities:
turbolent marked this conversation as resolved.
Show resolved Hide resolved

```cadence
// Get all capability controllers for capabilities that target this storage path
fun getControllers(path: StoragePath): [CapabilityController]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might make sense to add an iteration API here to avoid allocating a potentially very large array:

Suggested change
fun getControllers(path: StoragePath): [CapabilityController]
fun getControllers(for path: StoragePath): [CapabilityController]
// Iterate over all capability controllers that target the given storage path
fun forEachController(for path: StoragePath, _ f: ((CapabilityController): Bool))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it better to have both, or to just have the iterator?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it better to have both, or to just have the iterator?

I think there is no good answer for this.

// Get capability controller for capability with the specified id
// If the id does not reference an existing capability
// or the capability does not target a storage path on this address, return nil
fun getController(capabilityId: UInt64): CapabilityController?
```

Some methods would be removed from the AuthAccount object as they are no longer needed:
janezpodhostnik marked this conversation as resolved.
Show resolved Hide resolved

```cadence
fun link<T: &Any>(_ newCapabilityPath: CapabilityPath, target: Path): Capability<T>?
fun getLinkTarget(_ path: CapabilityPath): Path?
fun unlink(_ path: CapabilityPath)
```

The method `getCapability` would be renamed to `issueCapability `to reflect the fact that a new capability is created every time.

And also from the PublicAccount:

```cadence
fun getCapability<T>(_ path: PublicPath): Capability<T>
fun getLinkTarget(_ path: CapabilityPath): Path?
```

This would remove all references to `Link`s.

### Impact of the solution

#### Changes for capability consumers

The following pattern:

```cadence
let publicAccount = getAccount(issuerAddress)
let countCap = publicAccount.getCapability<&{HasCount}>(/public/hasCount)
let countRef = countCap.borrow()!
countRef.count
```

Would change to:

```cadence
let publicAccount = getAccount(issuerAddress)
let countCap = publicAccount.borrow<Capability<&{HasCount}>>(/public/hasCount)!
let countRef = countCap.borrow()!
countRef.count
```
joshuahannan marked this conversation as resolved.
Show resolved Hide resolved

Or using the `borrowCapability `shorthand:

```cadence
let publicAccount = getAccount(issuerAddress)
let countRef = publicAccount.borrowCapability<&{HasCount}>(/public/hasCount)!
countRef.count
```

Consuming private capabilities would change in the way that capabilities are resources and must be stored as such. The borrowing part would be the same.

#### Changes for capability issuers

There would be more change on the issuer's side. Most notably creating a public capability would look like this.

```cadence
let countCap <- authAccount.issueCapability<&{HasCount}>(/storage/counter)
authAccount.save(<- countCap, to: /public/hasCount)
janezpodhostnik marked this conversation as resolved.
Show resolved Hide resolved
```

joshuahannan marked this conversation as resolved.
Show resolved Hide resolved
Unlinking and relinking issued capabilities would change to getting a CapCon and calling the appropriate methods.

```cadence
let capCon = authAccount.getController(capabilityId: capabilityId)
janezpodhostnik marked this conversation as resolved.
Show resolved Hide resolved

capCon.revoke()
// or
capCon.restore()
// or
capCon.relink(target: /storage/counter2)
```

This example assumes that the capability id is known. This is always the case for capabilities in the accounts public domain, since the account has access to those directly. For private capabilities that were given to someone else this can be achieved by keeping an on-chain or an off-chain list of capability ids and some extra identifying information (for example the address of the receiver of the capability). If no such list was kept, the issuer can use the information on the CapCons retrieved through`authAccount.getControllers(path: StoragePath)`to find the right id.
janezpodhostnik marked this conversation as resolved.
Show resolved Hide resolved

#### Capability Minters

In certain situations it is required that an issuer delegates issuing capabilities to someone else. In this case the following approach can be used.

Let's assume that the issuer defined an `AdminInterface` resource interface and a `Main` resource (besides the `Counter` and `HasCount` from previous examples).

```cadence
public resource interface AdminInterface {
fun createCountCap(): @Capability<&{HasCount}>
}
public resource Main : AdminInterface {
fun createCountCap(): @Capability<&{HasCount}> {
return <- self.account.getCapability<&{HasCount}>(/storage/counter)
joshuahannan marked this conversation as resolved.
Show resolved Hide resolved
}
}
```

The issuer can then store a `Main` resource in their storage and give the capability to call it to a trusted party. The trusted party can then create `&{HasCount} `capabilities at will.

```cadence
authAccount.save(<-create Main(), to: /storage/counterMinter)
let countMinterCap <- authAccount.getCapability(/storage/counterMinter)
countMinterCap //give this to a trusted party
janezpodhostnik marked this conversation as resolved.
Show resolved Hide resolved
turbolent marked this conversation as resolved.
Show resolved Hide resolved
```

It is worth noting that everytime the `AdminInterface` is used a CapCon is created in the issuers storage taking up some resources.

## Sources

1. Miller, Mark & Yee, Ka-ping & Shapiro, Jonathan. (2003). Capability Myths Demolished.
2. [Capability-Based Security](https://en.wikipedia.org/wiki/Capability-based_security)
3. [Flow Doc Site](https://docs.onflow.org/cadence/language/capability-based-access-control)
4. [Flow Doc Site: capability receiver](https://docs.onflow.org/cadence/design-patterns/#capability-receiver)
5. [Flow Forum Post](https://forum.onflow.org/t/private-capability-revoke/1997)