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

many: support mixed outcomes for permissions in prompting constraints #14581

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

olivercalder
Copy link
Member

@olivercalder olivercalder commented Oct 8, 2024

This PR is based on #14538, and is tracked internally by https://warthogs.atlassian.net/browse/SNAPDENG-32594. It addresses some of the problems discussed in that PR (such as #14538 (comment)), and more broadly in canonical/desktop-security-center#74. CC @sminez @juanruitina.

Let rule content constraints have heterogeneous outcomes and lifespans for different permissions in the constraints. As such, convert the list of permissions to a map from permission to permission entry, where the entry holds the outcome, lifespan, and duration/expiration for that particular permission, where previous those were global to the containing rule, rule contents, or patch contents.

However, the existing concept of replying "allow"/"deny" to a particular set of requested permissions is clear and simple. We want to keep outcome, lifespan, and duration as reply-scoped values, not permission-specific, when accepting prompt replies. So we need different types of constraints for prompt replies vs. rule contents.

The motivation behind this is so that we can have only a single rule for any given path pattern. We may have a situation where the user previously replied with "allow read /path/to/foo" and they're now prompted for write access, they need to be able to respond with "deny read /path/to/foo". If we only support a single outcome for any given rule, then we'd need two rules for the same path /path/to/foo. Thus, we need rules to support different outcomes for different permissions.

The same logic applies for lifetimes and expirations, though this adds additional complexity now that the concept of rule expiration is shifted to being permission-specific. We care about expired rules in two primary places: when loading rules from disk, we want to discard any expired rules, and when adding a new rule, we want to discard any expired permission entry for a rule which shares a pattern variant with the new rule. For cases where that expired permission entry had a conflicting outcome, we clearly can't have that, and we want to remove the expired permission entry from its containing rule as well, so as to avoid confusion for the user without them needing to check expiration timestamps. Even if the outcome of the expired entry matches that of the new rule's entry for the same permission, we still want to prune the expired permission from the old rule to avoid confusion. The complexity is around when a notice is recorded for a rule for which some permissions have expired. At the moment, the logic is that a notice is recorded in these cases:

  • when a rule is loaded from disk
    • data may be "removed": "expired" if all permissions are expired
  • when a rule is added
  • when a rule is patched
  • when a rule is removed (with data "removed": "removed")
  • when a rule is found to be expired when attempting to add a new rule

Notably, a notice is not recorded automatically when a permission entry expires. Nor is a notice recorded when a permission is found to be expired, so long as its associated rule still has at least one non-expired permission. Neither pruning an expired permission entry from the rule tree nor from the entry's containing rule results in a notice, even though technically the rule data has changed, since the expired permission has been erased. The rationale is that the semantics of the rule have not changed, since the expiration of that permission was part of the semantics of the rule.

Since durations are used when adding/patching a rule and expirations are used when retrieving a rule, in addition to the differences for prompt replies vs. rule contents, we now need several different variants of constraints:

  • promptConstraints:
    • path, requested permissions list, available permissions list
    • internal to requestprompts, unchanged
  • ReplyConstraints:
    • path pattern, list of permissions
    • containing PromptReply holds outcome/lifespan/expiration
    • unchanged from before, though under a new name
    • converted to a Constraints if reply warrants a new rule
  • Constraints:
    • path pattern, map from permission to outcome, lifespan, duration
    • used when adding rule to the rule DB
    • converted to RuleConstraints when the new rule is created
  • RuleConstraints:
    • path pattern, map from permisison to outcome, lifespan, expiration
    • used when retrieving rules from the rule DB
    • never used when POSTing to the API
  • PatchConstraints:
    • identical to Constraints, but with omitempty fields
    • converted to RuleConstraints when the patched rule is created

To support this, we define some new types, including {,Rule}PermissionMap and {,Rule}PermissionEntry. The latter of these is used in the leaves of the rule DB tree in place of the previous set of rule IDs of rules whose patterns render to a given pattern variant.

Whenever possible, logic surrounding constraints, permissions, and expiration is pushed down to methods on these new types, thus simplifying the logic of their callers.

@olivercalder olivercalder added the Needs Samuele review Needs a review from Samuele before it can land label Oct 8, 2024
@github-actions github-actions bot added the Needs Documentation -auto- Label automatically added which indicates the change needs documentation label Oct 8, 2024
@olivercalder
Copy link
Member Author

TODO:

  • Add unit tests for new constraints types
  • Add requestrules unit test for partial rule expiration
  • Decide on handling of partial- and fully-expired rules when:
    • Getting rule by ID (currently no expiration checks)
    • Getting all rules (current discards fully expired rules, but does not prune expired permissions)
    • Adding rule which conflicts with expired rule permission (currently prunes expired rule permission and removes rule if all permissions are expired)

Allow several rules which render to the same variant to coexist in the
tree without conflict, so long as the outcome of all those overlapping
rules is identical.

This allows the client to reply with "allow read forever for /foo/bar"
and then later say "allow read|write forever for /foo/bar" without the
latter being treated as a rule conflict error. Clearly, the second rule
is a superset of the first, and there's no intent-based reason that
these two rules couldn't coexist, it was just an implementation detail
that we previously only allowed a pattern variant to be associated with
a single rule ID.

Now, each pattern variant in the tree for a particular snap, interface,
and permission can be associated with a set of rule IDs. Any non-expired
rules in that set must have the same outcome. Any expired rules in the
set are ignored (and removed when convenient).

Signed-off-by: Oliver Calder <[email protected]>
Let rule content constraints have heterogeneous outcomes and lifespans
for different permissions in the constraints. As such, convert the list
of permissions to a map from permission to permission entry, where the
entry holds the outcome, lifespan, and duration/expiration for that
particular permission, where previous those were global to the
containing rule, rule contents, or patch contents.

However, the existing concept of replying "allow"/"deny" to a particular
set of requested permisisons is clear and simple. We want to keep
outcome, lifespan, and duration as reply-scoped values, not
permission-specific, when accepting prompt replies. So we need different
types of constraints for prompt replies vs. rule contents.

The motivation behind this is so that we can have only a single rule for
any given path pattern. We may have a situation where the user
previously replied with "allow read `/path/to/foo`" and they're now
prompted for write access, they need to be able to respond with "deny
read `/path/to/foo`". If we only support a single outcome for any given
rule, then we'd need two rules for the same path `/path/to/foo`. Thus,
we need rules to support different outcomes for different permissions.

The same logic applies for lifetimes and expirations, though this adds
additional complexity now that the concept of rule expiration is shifted
to being permission-specific. We care about expired rules in two primary
places: when loading rules from disk, we want to discard any expired
rules, and when adding a new rule, we want to discard any expired
permisison entry for a rule which shares a pattern variant with the new
rule. For cases where that expired permission entry had a conflicting
outcome, we clearly can't have that, and we want to remove the expired
permission entry from its containing rule as well, so as to avoid
confusion for the user without them needing to check expiration
timestamps. Even if the outcome of the expired entry matches that of the
new rule's entry for the same permission, we still want to prune the
expired permission from the old rule to avoid confusion. The complexity
is around when a notice is recorded for a rule for which some
permissions have expired. At the moment, the logic is that a notice is
recorded in these cases:

- when a rule is loaded from disk
    - data may be `"removed": "expired"` if all permissions are expired
- when a rule is added
- when a rule is patched
- when a rule is removed (with data `"removed": "removed"`)
- when a rule is found to be expired when attempting to add a new rule

Notably, a notice is not recorded automatically when a permission entry
expires. Nor is a notice recorded when a permission is found to be
expired, so long as its associated rule still has at least one
non-expired permission. Neither pruning an expired permission entry from
the rule tree nor from the entry's containing rule results in a notice,
even though technically the rule data has changed, since the expired
permission has been erased. The rationale is that the semantics of the
rule have not changed, since the expiration of that permission was part
of the semantics of the rule.

Since durations are used when adding/patching a rule and expirations are
used when retrieving a rule, in addition to the differences for prompt
replies vs. rule contents, we now need several different variants of
constraints:
- `promptConstraints`:
    - path, requested permissions list, available permissions list
    - internal to `requestprompts`, unchanged
- `ReplyConstraints`:
    - path pattern, list of permissions
    - containing `PromptReply` holds outcome/lifespan/expiration
    - unchanged from before, though under a new name
    - converted to a `Constraints` if reply warrants a new rule
- `Constraints`:
    - path pattern, map from permission to outcome, lifespan, duration
    - used when adding rule to the rule DB
    - converted to `RuleConstraints` when the new rule is created
- `RuleConstraints`:
    - path pattern, map from permisison to outcome, lifespan, expiration
    - used when retrieving rules from the rule DB
    - never used when POSTing to the API
- `PatchConstraints`:
    - identical to `Constraints`, but with omitempty fields
    - converted to `RuleConstraints` when the patched rule is created

To support this, we define some new types, including `{,Rule}PermissionMap`
and `{,Rule}PermissionEntry`. The latter of these is used in the leaves
of the rule DB tree in place of the previous set of rule IDs of rules
whose patterns render to a given pattern variant.

Whenever possible, logic surrounding constraints, permissions, and
expiration is pushed down to methods on these new types, thus
simplifiying the logic of their callers.

Signed-off-by: Oliver Calder <[email protected]>
@olivercalder olivercalder force-pushed the prompting-mixed-outcomes-per-permission branch from 82e0611 to ac6c4e2 Compare October 8, 2024 04:05
@olivercalder olivercalder added this to the 2.67 milestone Oct 8, 2024
Copy link

codecov bot commented Oct 8, 2024

Codecov Report

Attention: Patch coverage is 48.47775% with 220 lines in your changes missing coverage. Please review.

Project coverage is 78.75%. Comparing base (ac897ee) to head (ac6c4e2).
Report is 55 commits behind head on master.

Files with missing lines Patch % Lines
interfaces/prompting/constraints.go 2.42% 200 Missing and 1 partial ⚠️
...erfaces/prompting/requestprompts/requestprompts.go 85.50% 7 Missing and 3 partials ⚠️
interfaces/prompting/requestrules/requestrules.go 93.28% 6 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #14581      +/-   ##
==========================================
- Coverage   78.85%   78.75%   -0.10%     
==========================================
  Files        1079     1083       +4     
  Lines      145615   146291     +676     
==========================================
+ Hits       114828   115215     +387     
- Misses      23601    23883     +282     
- Partials     7186     7193       +7     
Flag Coverage Δ
unittests 78.75% <48.47%> (-0.10%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Documentation -auto- Label automatically added which indicates the change needs documentation Needs Samuele review Needs a review from Samuele before it can land
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant