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

FLIP: Capability Controllers #798

merged 45 commits into from
May 12, 2023

Conversation

janezpodhostnik
Copy link
Contributor

Description

Cadence encourages a capability-based security model as described on the Flow doc site. 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 make this concept 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.

@janezpodhostnik janezpodhostnik self-assigned this Feb 3, 2022
@vercel
Copy link

vercel bot commented Feb 3, 2022

This pull request is being automatically deployed with Vercel (learn more).
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: https://vercel.com/onflow/flow-docs/J5dNbSkiW9GF1KvNN9tup7dnp3ZT
✅ Preview: https://flow-docs-git-janez-capability-controllers-onflow.vercel.app

@bluesign
Copy link
Contributor

bluesign commented Feb 4, 2022

I am super excited about this, thanks for this FLIP @janezpodhostnik

Few comments and ideas I have.

  • Capabilities as Resource makes little sense in my opinion. As I can Proxy the capability with resource. ( nested resource in another resource, and give capability to this resource to someone else )

  • I think Capability should be just struct(address, capabilityID) with borrow method. ( which should point to CapCon basically at address capabilityID, but maybe to prevent later confusion with capabilities / controllers we can merge both conceptually.

Technically AuthAccount.capabilities can have different type like [&AuthCapability] can allow revoke, restore etc methods, while PublicAccount.capabilities can be [Capability] ) here: AuthCapability is CapCon

  • Adding a description field to CapCon

  • Getting rid of private path is great, what if we get rid of public too ? ( I am a bit hater of Path's in general )
    ( For Public capabilities )

Something like this as a result:

Issue:

authAccount.issueCapability<&{HasCount}>(public: True, description: "HasCount Public")

This will create CapCon ( AuthCapability ) in user Storage which publicAccount.capabilities will contain a Capability to this CapCon ( AuthCapability ) also AuthAccount.capabilities will contain a &AuthCapability to this.

Use:

let publicAccount = getAccount(issuerAddress)
let countCapabilities = publicAccount.capabilities.filter<&{HasCount}>()
let countRef = countCapabilities[0]!.borrow()
countRef.count

For Private:

var countCap = authAccount.issueCapability<&{HasCount}>(public: False, description: "HasCount Capability to Janez")

What do you think ?

@turbolent turbolent added Cadence FLIP Flow Improvement Proposal labels Feb 8, 2022
@janezpodhostnik
Copy link
Contributor Author

@bluesign thank you for the comments!

Capabilities as Resource makes little sense in my opinion. As I can Proxy the capability with resource. ( nested resource in another resource, and give capability to this resource to someone else )

That is true, but the difference in this scenario is that if:

  • capability is a value: giving a capability to A who then copies to B then revoking the capability revokes this capability for A and B which might be surprising
  • capability is a resource: if A passes the capability to B ( by giving B a reference to a capability or by indirection) it might be more explicit that B's capability depends on A's capability

Adding a description field to CapCon

I like this, but should probably be an optional. Not sure it is a good idea to force descriptions.

Technically AuthAccount.capabilities can have different type like [&AuthCapability] can allow revoke, restore etc methods, while PublicAccount.capabilities can be [Capability] ) here: AuthCapability is CapCon

This might be easier to understand! What I'm thinking though, would this make it easier for people to make mistakes (by accidentally giving people the access to &AuthCapability). I have to think through this.

Getting rid of private path is great, what if we get rid of public too ? ( I am a bit hater of Path's in general ) ( For Public capabilities )

I like your example, but I have a problem with it. The public domain paths sort of serve as a name... so when you publicAccount.capabilities.filter<&{HasCount}>() and you get back two capabilities, how do you know which one to choose? By description? that seems error prone.

@sideninja
Copy link
Member

sideninja commented Feb 18, 2022

I think this is a great improvement. To me handling capabilities always felt complex and abstracting that logic with a nice API would be awesome.
I'm just brainstorming but how about if the API also allows listing all capability controllers account has? not just for a specific storage path. Honestly just an idea since I don't use cadence enough to know if that is actually used a lot, but just thinking to have an option where you can check what a certain account "supports".

@bluesign
Copy link
Contributor

hey @janezpodhostnik

Thanks for the answers, sorry I missed the answers when you posted, thanks to @sideninja I just saw.

Resource vs Struct discussion, I think we have just opposite perspective, which is very nice, I think both sides have cons/pros. My main perspective here was, giving a resource, creating a false sense of security and uniqueness. Resources have inherited "unique" from their definition, which can give more assurances than it should be. But I am ok with resources, maybe just would need to document.

I like your example, but I have a problem with it. The public domain paths sort of serve as a name... so when you publicAccount.capabilities.filter<&{HasCount}>() and you get back two capabilities, how do you know which one to choose? By description? that seems error prone.

The problem here is in my opinion: Paths are more error prone. Actually this is more of a business logic problem than technical.

Imagine this: if I am a wallet developer, user has 2 FlowVaults, what should wallet do ?

  • Use primary one linked at default Path ( don't allow to use other Vault )
  • Enumerate Wallets, show them with Paths ( or better with descriptions )

Now for me obvious better solution is the second one, but I feel Flow pushing people to use first one.

Copy link
Member

@joshuahannan joshuahannan left a comment

Choose a reason for hiding this comment

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

Great start. I left a bunch of comments, mostly grammar, formatting, but a few suggestions.

For the other discussions above, I feel better about them usually being resources, because while it is possible for people to just get around it, it makes it a lot more difficult than if it were a struct. I could potentially support allowing users to specify if they want to create a non-resource capability in the public domain to make it optional.

I also fully support removing private paths, but I am a little undecided on what we should do about the paths vs types point that @bluesign made. paths are definitely error prone, but @janezpodhostnik 's point about not knowing which vault to use is a good one.

Also, an array of CapCons could potentially get expensive if you need to iterate to find one. Any reason it can’t be a dictionary?
Could still iterate like with an array, but could also index.

flips/20220203-capability-controllers.md Outdated Show resolved Hide resolved
flips/20220203-capability-controllers.md Outdated Show resolved Hide resolved
flips/20220203-capability-controllers.md Outdated Show resolved Hide resolved
flips/20220203-capability-controllers.md Outdated Show resolved Hide resolved
flips/20220203-capability-controllers.md Outdated Show resolved Hide resolved
flips/20220203-capability-controllers.md Outdated Show resolved Hide resolved
flips/20220203-capability-controllers.md Outdated Show resolved Hide resolved
flips/20220203-capability-controllers.md Show resolved Hide resolved
flips/20220203-capability-controllers.md Outdated Show resolved Hide resolved
flips/20220203-capability-controllers.md Outdated Show resolved Hide resolved
@bjartek
Copy link
Contributor

bjartek commented Mar 8, 2022

@bluesign just made me aware of this.

Will this not break a lot of code. Today I store Capabilities inside structs very often and this change will make this impossible. Since a Capability is a resource and I cannot store a reference to it.

Or am I missing something obivious here?

@joshuahannan
Copy link
Member

Will this not break a lot of code.

It will break a lot of code in theory. We're figuring out a proposal first that we can all agree would be useful, then we'll discuss more about the feasibility of upgrades and migrations to address the breaking changes. We still have a lot of control over upgrades and migrations, so it might not be as terrible as it sounds. Still needs to be discussed a lot though

@bjartek
Copy link
Contributor

bjartek commented Mar 9, 2022

Right now there is no event emitted when storage is stored/loaded or when a link/unlink operation happends. It would be very powerful if that is the case.

One constant problem that is an edge case you need to handle in cadence is the posibility for blackhat to craft his own transaction and unlink something. If you do not think about this beforehand then your entire system can grind down to a halt. Now having the posibility to listen to those events to see when it happends will atleast make the situation a little better.

Also knowing "who has set up a collection of the given type" would also be possible if there was events emitted.

@bluesign
Copy link
Contributor

bluesign commented Mar 9, 2022

@bjartek it would be nice, I suggested that before ( onflow/cadence#1059 )

But I think there are some technical limitations preventing that to be implemented. Events per block, events per transaction etc. TPS vs usability trade-offs

@aishairzay
Copy link
Contributor

This FLIP introducing new capability functions may be a good spot to add support for run-time types as part of capability functions: onflow/cadence#1617

This is to allow for the following to work:

let publicAccount = getAccount(issuerAddress)
let runTimeType: Type = Type<&{NonFungibleToken.CollectionPublic}>()
let countRef = publicAccount.borrowCapability<runTimeType>(/public/hasCount)! // or publicAccount.borrowCapability(/public/hasCount, type: runtimeType)
countRef.count

Is this something that would be feasible to include here?

@turbolent
Copy link
Member

@aishairzay

Borrowing using a run-time type will not be possible, as the type checker needs to be able to statically know what the result of the function is going to be.

However, a function to create a capability using a run-time type could be added, as the type does not need to be statically known, just at run-time.

Co-authored-by: Joshua Hannan <joshua.hannan@dapperlabs.com>
@vercel
Copy link

vercel bot commented May 17, 2022

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated
flow-docs ✅ Ready (Inspect) Visit Preview Aug 22, 2022 at 2:05PM (UTC)

flips/20220203-capability-controllers.md Outdated Show resolved Hide resolved
flips/20220203-capability-controllers.md Outdated Show resolved Hide resolved
flips/20220203-capability-controllers.md Outdated Show resolved Hide resolved
flips/20220203-capability-controllers.md Outdated Show resolved Hide resolved
flips/20220203-capability-controllers.md Outdated Show resolved Hide resolved
flips/20220203-capability-controllers.md Outdated Show resolved Hide resolved
flips/20220203-capability-controllers.md Outdated Show resolved Hide resolved
flips/20220203-capability-controllers.md Outdated Show resolved Hide resolved
flips/20220203-capability-controllers.md Outdated Show resolved Hide resolved
flips/20220203-capability-controllers.md Outdated Show resolved Hide resolved
flips/20220203-capability-controllers.md Outdated Show resolved Hide resolved
flips/20220203-capability-controllers.md Outdated Show resolved Hide resolved
@turbolent
Copy link
Member

@janezpodhostnik Could you please provide a summary of the current discussion, i.e. what the current sentiment is, and if and what any outstanding questions are?

@katelynsills
Copy link

Hi @janezpodhostnik, @turbolent asked me to comment on this FLIP. I’ll share some thoughts, but I’m sure I’m missing some nuances due to relative unfamiliarity with Cadence, so please feel free to correct my misconceptions!

First, this is a wonderfully clear document. The example code with private and public sharing of capabilities was especially helpful.

Second, I tried to summarize what I saw as the problems and pain points below. Is there anything I’m getting wrong or missing? I'll follow up with another comment.

Problems

  1. Confusing and awkward syntax: Capabilities syntax is currently confusing and awkward to use
  2. Accidental Relinking: Relinking can be done accidentally by linking with the same path after it has been unlinked
  3. Redirecting is not explicit: redirecting is done by linking again with the same path but it can be indistinguishable from accidental relinking even though the user's intent is very different
  4. Shared Access To Capabilities: If an issuer creates a capability and sends it to Person A, Person A can share access to the capability merely by creating a copy and giving it to Person B without asking the issuer’s permission
    • Not always a problem: Sometimes sharing without having to ask permission is desirable, such as a tip jar capability that allows anyone to give money
    • However, other times shared access is undesirable:
      • The issuer might not want Person B to have access, because they believe Person B is likely to abuse the access or use it differently than intended
      • Person A might worry that Person B will misuse the capability, causing both of their access to be cut off entirely because the issuer couldn’t tell who was responsible.
      • The community as a whole might suffer because being unable to manage access well could cause hesitancy in sharing access to anyone. (For example, a wiki that can’t tell who is responsible for vandalism can’t easily allow the public to edit.)
    • Note: as we know, sharing access can’t actually be prevented entirely in practice, because people can always let other people log in as them
  5. Revoking a particular person’s access is difficult: Unlinking a path revokes all copies of the capability using that path. In order to revoke only for a particular person, you would have to create multiple private linked paths (e.g. /private/hasCountAlice, /private/hasCountBob)
  6. No recording of policy or intention when creating a capability: there is currently no way to record why a path was created other than including hints in the path name, making it difficult to know why a certain capability was issued
  7. Must Create Receivers: Must create a receiver for each new capability type, with its own public path

@katelynsills
Copy link

My comment started to get too long, so I'm dividing it up. :)

Cutting off access to capabilities

There’s some prior literature in capability-based security on cutting off access to capabilities because of misuse:

  1. The paper Delegating Responsibility in Digital Systems: Horton’s “Who Done It?”
  2. The related talk by Mark Miller at Activity Pub 2019: ​​https://youtu.be/NAfjEnu6R2g?t=573

I’ll attempt to summarize here, but one helpful distinction from the paper is proactive control vs reactive control:

  • Proactive control: prevent bad things from happening, or limit the damage when they do
  • Reactive control: cut off future access, punish

Capabilities support proactive control well, since access and damage can be limited because (when done well) authority is already limited to the least amount needed to carry out the request. But, capabilities in their basic form don’t provide any information about who is to blame for misusing access, which is required for reactive control.

To add reactive control to capability-based security, the paper suggests an approach called HORTON. Here’s a simplified explanation of HORTON, using the same scenario as in my previous comment, where an issuer creates a capability for Person A, and Person B asks Person A for access:

  1. Person A says to the issuer: “I’d like to share my access to the capability with Person B, could you create separate access for Person B?”
  2. The issuer creates a new capability for Person B, and tags the capability with Person B’s identifier (for Cadence, this is probably an address)
  3. The issuer sends the new capability to Person B directly (or through Person A, but in that case we must guarantee that Person A isn’t able to use Person B’s new capability, so that Person B isn’t unfairly blamed for A’s behavior.)
  4. If the capability that was given to Person B is misused, the issuer can revoke that capability specifically, without necessarily cutting off access for Person A.
  5. The issuer can find all capabilities tagged with Person B’s address, and choose to revoke all of those as well.
  6. The issuer has recorded how Person B was introduced (through A) and therefore might want to reconsider access for Person A too.
  7. Similarly, Person B can revoke access in the opposite direction. Person B has tagged their capabilities with the particular issuer, and if B decides that those capabilities are untrustworthy, B can choose to shut off their own access to the issuer by finding all their capabilities tagged with the issuer’s address.

I’ll follow up in later comments to connect this to CapCons, but I wanted to post this separately in case it triggered any ideas itself.

@janezpodhostnik
Copy link
Contributor Author

Was there a particular reason to not just make these function a settable field pub(set) var target: StoragePath?

I was assuming some code would need to be run when the target is set, so I opted for a function. But I have no strong opinion about it. It can be a field if that fits better

@bluesign
Copy link
Contributor

bluesign commented Apr 22, 2023

Was there a particular reason to not just make these function a settable field pub(set) var target: StoragePath

I am in favor of this in general, however I think that instead of public fields, we should follow styles defined in contract standards (such as FT, NFT, Metadata etc ) and use getters and setters. Doing so would bring some benefits in terms of consistency.

Copy link
Member

@joshuahannan joshuahannan left a comment

Choose a reason for hiding this comment

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

Looks good! I'm fine with the rollout plan

flips/20220203-capability-controllers.md Show resolved Hide resolved
flips/20220203-capability-controllers.md Show resolved Hide resolved
@turbolent
Copy link
Member

I've updated the FLIP by documenting the new migration plan, adding tags to controllers (this was a feature which was requested in breakout sessions), and improved some wording.

@turbolent
Copy link
Member

turbolent commented May 10, 2023

While reviewing the implementation of this proposal, the question of how mutations are handled/supported during iteration using the forEachController functions came up, i.e. issuing new capability controllers or deleting existing ones. The problem is that mutations (issuing or deleting) may influence the iteration order after the mutation.

There are several options for the behaviour:

  • Allow issuing/deleting of controllers while iterating and leave iteration implementation defined. This is efficient, but may lead to surprising iteration behaviour after a mutation, e.g. existing controllers may get skipped, existing controllers may be re-visited, new controllers may or may not get visited.

  • Allow issuing/deleting of controllers while iterating, but ensure ordering is consistent, e.g. by snapshotting the set of controllers to be visited at the beginning of iteration, which ensures that all existing controllers will be visited, but also, controllers which are issued during iteration will not. This is potentially expensive (potentially even prohibitively), if there are many capability controllers. The function would be equivalent to using the getControllers function and looping over the result.

  • Forbid issuing/deleting of controllers while iterating. This would ensure that ordering is consistent and that all existing controllers will be visited. It would also be efficient for the case where there are many controllers.
    However, it could potentially make some (common?) operations more difficult to implement. For example, to delete certain/all controllers, one would have to e.g. construct a temporary array to accumulate all controllers which should get deleted.

@dsainati1
Copy link
Contributor

Between the three options presented here, my preference is for the third.

The first option is untenable IMO, we elected not to use this behavior for all of our previous iterator implementations (storage, attachments), and it would be problematic here for the same reasons as there.

Between the latter two, I think given your point that the snapshotting functionality would be equivalent to calling getControllers and iterating over the result, I think it would be fine to simply forbid mutation during iteration. This is more efficient in the cases where mutation is not desired, and if mutation is required then the user can simply iterate over the result of getControllers and perform mutation operations on capability storage which will not be reflected in the iterator.

@bluesign
Copy link
Contributor

Between the three options presented here, my preference is for the third.

I agree with @dsainati1 here, we already have getControllers for storagePath. Using it makes more sense when deleting etc.

@turbolent
Copy link
Member

@dsainati1 @bluesign Thank you for the feedback! Agreed with that reasoning, I modified the behaviour of the implementation and will update the FLIP to reflect it

flips/20220203-capability-controllers.md Outdated Show resolved Hide resolved
flips/20220203-capability-controllers.md Outdated Show resolved Hide resolved
@turbolent
Copy link
Member

Given that there has been no further feedback or concerns, this FLIP has been accepted 🎉

Thank you everyone who participated in the FLIP review process for the great discussions and valuable feedback! Your input has helped make this proposal a solid improvement over the existing linking-based capability API.

The implementation of this proposal in Cadence has been completed, and is awaiting to get reviewed and merged. Once merged, it is going to be included in an Emulator preview release, at which point additional documentation will be provided. This will allow developers to adopt the new Cap Cons API, migrate from the existing linking API, and provide feedback on the implementation.

Finally, the plan is to deploy the feature in an upcoming network upgrade (TBD), in advance of the Stable Cadence release, which will automatically migrate remaining capabilities and links and remove the old linking API.

If you have any questions or concerns, please do not hesitate to reach out!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Cadence FLIP Flow Improvement Proposal S-Governance
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet