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

Optional secret for Assignable CapAccess #3859

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from

Conversation

maackle
Copy link
Member

@maackle maackle commented May 13, 2024

Summary

This came out of a documentation discussion. Doesn't this seem preferable?

Also looks like there was a bug, which this fixes

TODO:

  • CHANGELOGs updated with appropriate info

Comment on lines -119 to -120
CapAccess::Transferable { secret, .. } => check_secret.map(|given| secret == given).unwrap_or(false),
CapAccess::Assigned { secret, .. } => check_secret.map(|given| secret == given).unwrap_or(false),
Copy link
Member Author

Choose a reason for hiding this comment

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

I think this was a bug. If check_secret is None, then this function would always just return false, but actually it should have the effect of skipping the secret check. Not sure if we ever use this code path but my change seems correct.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see this function referenced a few times throughout the codebase, and the app API at least accepts an optional secret, so I imagine it would hit this code path. But wouldn't you need it to return false for a None secret if it's a transferrable grant, or unwrap_or(secret.is_none()) if it's an assigned grant?

(an interesting edge case question is -- what if a secret is supplied and the grant doesn't need one? Should it be true still, even if it doesn't match?)

Copy link
Contributor

Choose a reason for hiding this comment

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

Well, if this function named is_valid is being called for a Transferable cap grant but with check_secret == None then the result cannot really be represented with a boolean in the first place, can it? It's rather an "undetermined" state - so maybe the function should be called is_validated instead (small joke ;-) )?
So I see how one could reason to return false if no secret is provided for a Transferable cap grant to protect against this function accidentally being called for a transferable grant without providing the secret.

Copy link
Contributor

Choose a reason for hiding this comment

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

@maackle after reading your comment in light of @matthme 's comment I think I see what you're saying. The effect of skipping the check would be that the loop that checks all the grants on the source chain would also come up with nothing and return false, right? I'm wondering when are the times that the secret would be None -- AFAICT it's only when the caller didn't supply one, in which case I think this check should probably return false rather than getting skipped (although the effect is the same -- access denied).

Copy link
Contributor

Choose a reason for hiding this comment

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

If check_secret is None, then this function would always just return false

For unrestricted cap grants it returns true, and the other two variants required a secret, so you couldn't skip the secret check. It seemed correct to me.

If the secret becomes optional for assigned cap grants, the logic can no longer be used. But the update is wrong, because a transferable cap grant still requires a secret, and now if none is provided, the secret check is skipped.

Copy link
Contributor

Choose a reason for hiding this comment

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

this check should probably return false rather than getting skipped (although the effect is the same -- access denied).

In order to properly implement 'skipped' logic in a way that doesn't let SourceChain::valid_cap_grant allow unauthorised people access a function by supplying a None secret, this function would have to have a return signature of Option<bool> and let its consumer treat None as equivalent to false in the loop... (which I guess is what you're saying @matthme ) I'd just as soon return false if a required secret wasn't supplied.

Copy link
Member Author

Choose a reason for hiding this comment

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

I made the mistake of interpreting check_secret as an imperative i.e. if it's Some, we want to check the secret, if it's None, we don't want to check the secret, and it's the responsibility of the caller to decide whether they want to check the secret or not. I did this too casually, looking more deeply I see that check_secret comes directly from the zome caller, so we can't give them that responsibility.

Let me try again.

Copy link
Member Author

Choose a reason for hiding this comment

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

I updated this, check my work

Copy link
Contributor

Choose a reason for hiding this comment

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

LGTM. It would be great to have tests for this method. Are you willing to add them? Otherwise I would.

Copy link
Member Author

Choose a reason for hiding this comment

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

Test written.

Comment on lines -119 to -120
CapAccess::Transferable { secret, .. } => check_secret.map(|given| secret == given).unwrap_or(false),
CapAccess::Assigned { secret, .. } => check_secret.map(|given| secret == given).unwrap_or(false),
Copy link
Contributor

Choose a reason for hiding this comment

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

I see this function referenced a few times throughout the codebase, and the app API at least accepts an optional secret, so I imagine it would hit this code path. But wouldn't you need it to return false for a None secret if it's a transferrable grant, or unwrap_or(secret.is_none()) if it's an assigned grant?

(an interesting edge case question is -- what if a secret is supplied and the grant doesn't need one? Should it be true still, even if it doesn't match?)

@@ -359,7 +359,7 @@ fixturator!(
CapAccessVariant::Unrestricted => CapAccess::from(()),
CapAccessVariant::Transferable => CapAccess::from(CapSecretFixturator::new_indexed(Empty, get_fixt_index!()).next().unwrap()),
CapAccessVariant::Assigned => CapAccess::from((
CapSecretFixturator::new_indexed(Empty, get_fixt_index!()).next().unwrap(),
Some(CapSecretFixturator::new_indexed(Empty, get_fixt_index!()).next().unwrap()),
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be more semantically correct to use None for an empty fixture? (I have no clue; just applying beginner's mind here)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, that would probably be better

@@ -376,7 +376,7 @@ fixturator!(
let number_of_assigned = rng.gen_range(0..5);

CapAccess::from((
CapSecretFixturator::new_indexed(Unpredictable, get_fixt_index!()).next().unwrap(),
Some(CapSecretFixturator::new_indexed(Unpredictable, get_fixt_index!()).next().unwrap()),
Copy link
Contributor

Choose a reason for hiding this comment

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

Likewise, wondering if None would also be a valid point in the unpredictable curve.

Copy link
Member Author

Choose a reason for hiding this comment

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

It would, but it's onerous to make an fixturator for an Option of something, so I opted to just replicate the previous behavior.

Copy link
Contributor

Choose a reason for hiding this comment

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

got it. Even knowing nothing about how fixt works, I kinda anticipated that it would be complicated.

Comment on lines -119 to -120
CapAccess::Transferable { secret, .. } => check_secret.map(|given| secret == given).unwrap_or(false),
CapAccess::Assigned { secret, .. } => check_secret.map(|given| secret == given).unwrap_or(false),
Copy link
Contributor

Choose a reason for hiding this comment

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

If check_secret is None, then this function would always just return false

For unrestricted cap grants it returns true, and the other two variants required a secret, so you couldn't skip the secret check. It seemed correct to me.

If the secret becomes optional for assigned cap grants, the logic can no longer be used. But the update is wrong, because a transferable cap grant still requires a secret, and now if none is provided, the secret check is skipped.

// Unless the extern is unrestricted.
CapAccess::Unrestricted => true,
// note the PartialEq implementation is constant time for secrets
CapAccess::Transferable { secret, .. } => secret == given_secret,
Copy link
Contributor

Choose a reason for hiding this comment

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

Transferable requires a secret, the check must not be skipped by not providing one.

Copy link
Contributor

@pdaoust pdaoust left a comment

Choose a reason for hiding this comment

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

Some positive feedback and a non-blocking suggestion.

check_function: &GrantedFunction,
check_agent: &AgentPubKey,
check_secret: Option<&CapSecret>,
given_function: &GrantedFunction,
Copy link
Contributor

Choose a reason for hiding this comment

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

I like this renaming. Makes the intent of the code much clearer -- I was having to refer back a few times when I was reading it before.

@maackle maackle requested review from pdaoust and jost-s May 20, 2024 03:33
Copy link
Contributor

@jost-s jost-s left a comment

Choose a reason for hiding this comment

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

Yay!

It makes me doubt that passing in a wrong secret when none is required is valid. Fine anyway.

let secret_wrong: CapSecret = [2; 64].into();
let tag = "tag".to_string();

let g1: CapGrant = ZomeCallCapGrant {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
let g1: CapGrant = ZomeCallCapGrant {
let transferable: CapGrant = ZomeCallCapGrant {

functions: GrantedFunctions::All,
}
.into();
let g2: CapGrant = ZomeCallCapGrant {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
let g2: CapGrant = ZomeCallCapGrant {
let assigned_to_agent_1_no_secret: CapGrant = ZomeCallCapGrant {

functions: GrantedFunctions::All,
}
.into();
let g3: CapGrant = ZomeCallCapGrant {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
let g3: CapGrant = ZomeCallCapGrant {
let assigned_to_agent_1_with_secret: CapGrant = ZomeCallCapGrant {

@maackle
Copy link
Member Author

maackle commented May 21, 2024

@pdaoust want to re-review?

@maackle maackle dismissed pdaoust’s stale review May 21, 2024 18:35

needs re-review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants