-
Notifications
You must be signed in to change notification settings - Fork 582
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
base: master
Are you sure you want to change the base?
many: support mixed outcomes for permissions in prompting constraints #14581
Conversation
TODO:
|
82e0611
to
ac6c4e2
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #14581 +/- ##
==========================================
+ Coverage 78.95% 79.00% +0.04%
==========================================
Files 1084 1086 +2
Lines 146638 147787 +1149
==========================================
+ Hits 115773 116753 +980
- Misses 23667 23804 +137
- Partials 7198 7230 +32
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
ac6c4e2
to
a191514
Compare
Now that canonical#14581 has landed, rules may overlap as long as their outcomes do not conflict. As such, the download_file_defaults test case is no longer expected to fail. Signed-off-by: Oliver Calder <[email protected]>
@olivercalder this needs a rebase now? |
a191514
to
b2d5acc
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
did a pass on about half of this, some initial questions/comments
// dedicated PermissionEntry values for each permission in the reply. | ||
// Outcome and lifespan are validated while unmarshalling, and duration is | ||
// validated against the given lifespan when constructing the Constraints. | ||
constraints, err := replyConstraints.ToConstraints(prompt.Interface, outcome, lifespan, duration) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can't constraints be used more through the function? and if not, why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Match
and ContainPermissions
methods could be moved to the Constraints
type instead of the ReplyConstraints
type, but they're really about validating that the reply is well-formed, which is specific to ReplyConstraints
rather than Constraints
. We don't in general have a reason to check whether Constraints
match a particular path or set of permissions --- that is the role of RuleConstraints
. And further, I don't think it makes as much sense to check whether a rule has an entry for each permission in the list, since those entries could have mixed outcomes which wouldn't have come from a single reply, since replies always have a single outcome.
Basically, my motivation is about keeping the methods about validating replies (ToConstraints
, Match
, and ContainsPermissions
) to ReplyConstraints
, so there's no risk of accidentally mis-using them on Constraints
in other situations. E.g. one never wants to match an incoming request against Constraints
, as incoming requests should only be matched against RuleConstraints
, and Constraints
must always be converted to RuleConstraints
. ReplyConstraints
may be matched against existing requests to make sure they satisfy everything which was requested.
As for why replyConstraints.ToConstraints
occurs before replyConstraints.Match
and replyConstraints.ContainPermissions
, the former validates that the reply is well-formed in the basic sense, while the latter two checks that it's semantically valid by satisfying the original request.
Does this address your question? Or is there something else I'm missing?
if currTime.After(expiration) { | ||
return fmt.Errorf("%w: %q", prompting_errors.ErrRuleExpirationInThePast, expiration) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this just a simplicication?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that rules can have mixed outcomes/lifespans/expirations, it may be the case that (e.g. when reading from disk) one permission has expired and another one has not. Rather than throwing an error when an expired permission is seen, it instead is removed from the rule at a later step, and any non-expired permissions remain.
In particular: ValidateExpiration
is only called once, from within RulePermissionEntry.validate()
, which is in turn only called once, in RulePermissionMap.validateForInterface()
. Before calling entry.validate()
, validateForInterface
first checks entry.Expired()
, and if the permission entry has expired, the permission is removed from the permission map at the end of the function, after ensuring that no other errors occurred.
So not quite a simplification, it's a change in the distinction between expired rules and invalid rules now that rules can be "partially expired" but still valid.
RuleIDs map[prompting.IDType]bool | ||
Variant patterns.PatternVariant | ||
Outcome prompting.OutcomeType | ||
RuleEntries map[prompting.IDType]*prompting.RulePermissionEntry |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is RulePermissionEntry defined outside of here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A RulePermissionMap
is a map from permission (string) to RulePermissionEntry
, so to avoid a circular dependency, RulePermissionEntry
must be defined in the same place RulePermissionMap
is (or in a lower level). But at the same time, RuleConstraints
contains a RulePermissionMap
, so RulePermissionMap
must be defined in the same place or lower in the chain than RuleConstraints
.
An argument could be made for defining RulePermissionMap
and RuleConstraints
in requestrules
instead of interfaces/prompting
. But requestprompts
uses RuleConstraints
as well, so then requestprompts
would need to import requestrules
, which is fine but a bit weird to me.
More importantly though, Constraints
are validated by being converted into RuleConstraints
. So wherever Constraints
is defined needs to import whatever defined RuleConstraints
. So I think RuleConstraints
, RulePermissionMap
, and RulePermissionEntry
all need to be defined in interfaces/prompting
.
An alternative could be to have separate validation and conversion functions, but these would be essentially identical (and the latter would need to call the former again, probably), so it seems to me like the code overlap would be great, and the conversion functions defined in requestrules
would need to operate on the internals of those structs as defined in interfaces/prompting
. So I think it's best to have all the similarly structured and related types defined in one place.
modified := prompt.Constraints.subtractPermissions(constraints.Permissions) | ||
if !modified { | ||
// No permission was matched | ||
// Matched, so at least one permission was satisfied |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand the comment vs the code, matched doesn't meant that all outcomes are not deny? it seems the comment needs to be clarified/expanded
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Matched means the path pattern of the rule matched the path of the prompt, and at least one permission from the rule matched at least one permission from the prompt. Whether all or just some of the permissions were matched, and whether each was allowed or denied, is what the other return values of matched, satisfied, denied, err := prompt.Constraints.applyRuleConstraints(constraints)
indicates.
But I agree this is rather confusing. matched
, satisfied
, and denied
are not the same types, and there is some implication between each.
I think the complexity comes down to the interaction between the way buildResponse
, applyRuleConstraints
, and their callers interact. A lot of it is because of the old way these used to work, which is no longer the case, so they can be simplified. I'll work on that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
couple more comments. I haven't reviewed requestrules.go changes yet, probably would be good to improve the rest first
// permissions which have a lifespan of "timespan". RulePermissionEntry is what | ||
// is returned when retrieving rule contents, but PermissionEntry is used when | ||
// replying to prompts, creating new rules, or modifying existing rules. | ||
type RulePermissionEntry struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this seems really the same with PermissionEntry, why the two types?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RulePermissionEntry
has an Expiration
field while PermissionEntry
has a Duration
field. When a user replies to a prompt or tries to create or patch a rule directly over the rules API, they always act in terms of duration, rather than expiration. Internally, those durations are validated and then converted to an expiration. But the client should never specify an expiration timestamp when POSTing or PATCHing to the API.
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]>
…ests Signed-off-by: Oliver Calder <[email protected]>
Signed-off-by: Oliver Calder <[email protected]>
Signed-off-by: Oliver Calder <[email protected]>
…ound handling new rules Signed-off-by: Oliver Calder <[email protected]>
…omes Signed-off-by: Oliver Calder <[email protected]>
663ab69
to
46b5c71
Compare
We need to be careful to support both the old and new rule formats during a transition period. The internals should use the new system, but we'll need to map the old format to the new structure, and provide a means of working in the old format over the API. |
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:
"removed": "expired"
if all permissions are expired"removed": "removed"
)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
:requestprompts
, unchangedReplyConstraints
:PromptReply
holds outcome/lifespan/expirationConstraints
if reply warrants a new ruleConstraints
:RuleConstraints
when the new rule is createdRuleConstraints
:PatchConstraints
:Constraints
, but with omitempty fieldsRuleConstraints
when the patched rule is createdTo 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.