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

Merged
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7fa28fd
many: support mixed outcomes for permissions in prompting constraints
olivercalder Oct 8, 2024
3c7d284
i/prompting: expire entry on (not after) timestamp and improve unit t…
olivercalder Nov 7, 2024
9d7208e
i/prompting: unexport `PermissionMap.ToRulePermissionMap`
olivercalder Nov 12, 2024
5bebcff
i/p/errors: added tests for errors.Join
olivercalder Nov 12, 2024
5ad2fc5
i/p/requestprompts: adjust comments and naming to clarify behavior ar…
olivercalder Nov 14, 2024
4b9170e
i/p/requestprompts: expand and simplify tests of rule with mixed outc…
olivercalder Nov 14, 2024
9a86f81
i/p/requestrules: add test case for partially expired rules when adding
olivercalder Dec 3, 2024
63dcf4e
i/prompting,o/i/apparmorprompting: move `Match` and `ContainPermissio…
olivercalder Dec 4, 2024
fc22f07
i/prompting: always validate inputs before handling simple cases
olivercalder Dec 9, 2024
8d60c26
i/p/requestprompts: move buildResponse to method on promptConstraints
olivercalder Dec 9, 2024
b5eb306
i/p/requestrules: clarify comments around rule patching
olivercalder Dec 9, 2024
5ba9463
i/p/requestrules: only prune expired permission from rule if rule is …
olivercalder Dec 9, 2024
04301f0
i/p/requestprompts: adjust variable names and comments
olivercalder Dec 10, 2024
bf1434a
i/prompting: add missing continue during rule permission map validation
olivercalder Dec 10, 2024
6d0dce6
i/prompting: adjust doc comments for various constraints types
olivercalder Dec 10, 2024
f557746
many: move prompting errors.Join to strutil.JoinErrors to avoid unwan…
olivercalder Dec 10, 2024
ffb6c9c
many: renamed `PatchConstraints` to `RuleConstraintsPatch`
olivercalder Dec 11, 2024
f554c2d
tests: update apparmor-prompting-snapd-startup for new rules format
olivercalder Dec 17, 2024
3ad9783
tests: update interfaces-snap-interfaces-requests-control for new rul…
olivercalder Dec 17, 2024
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
20 changes: 7 additions & 13 deletions daemon/api_prompting.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,19 +290,16 @@ var getInterfaceManager = func(c *Command) interfaceManager {
}

type postPromptBody struct {
Outcome prompting.OutcomeType `json:"action"`
Lifespan prompting.LifespanType `json:"lifespan"`
Duration string `json:"duration,omitempty"`
Constraints *prompting.Constraints `json:"constraints"`
Outcome prompting.OutcomeType `json:"action"`
Lifespan prompting.LifespanType `json:"lifespan"`
Duration string `json:"duration,omitempty"`
Constraints *prompting.ReplyConstraints `json:"constraints"`
}

type addRuleContents struct {
Snap string `json:"snap"`
Interface string `json:"interface"`
Constraints *prompting.Constraints `json:"constraints"`
Outcome prompting.OutcomeType `json:"outcome"`
Lifespan prompting.LifespanType `json:"lifespan"`
Duration string `json:"duration,omitempty"`
}

type removeRulesSelector struct {
Expand All @@ -311,10 +308,7 @@ type removeRulesSelector struct {
}

type patchRuleContents struct {
Constraints *prompting.Constraints `json:"constraints,omitempty"`
Outcome prompting.OutcomeType `json:"outcome,omitempty"`
Lifespan prompting.LifespanType `json:"lifespan,omitempty"`
Duration string `json:"duration,omitempty"`
Constraints *prompting.RuleConstraintsPatch `json:"constraints,omitempty"`
}

type postRulesRequestBody struct {
Expand Down Expand Up @@ -465,7 +459,7 @@ func postRules(c *Command, r *http.Request, user *auth.UserState) Response {
if postBody.AddRule == nil {
return BadRequest(`must include "rule" field in request body when action is "add"`)
}
newRule, err := getInterfaceManager(c).InterfacesRequestsManager().AddRule(userID, postBody.AddRule.Snap, postBody.AddRule.Interface, postBody.AddRule.Constraints, postBody.AddRule.Outcome, postBody.AddRule.Lifespan, postBody.AddRule.Duration)
newRule, err := getInterfaceManager(c).InterfacesRequestsManager().AddRule(userID, postBody.AddRule.Snap, postBody.AddRule.Interface, postBody.AddRule.Constraints)
if err != nil {
return promptingError(err)
}
Expand Down Expand Up @@ -542,7 +536,7 @@ func postRule(c *Command, r *http.Request, user *auth.UserState) Response {
if postBody.PatchRule == nil {
return BadRequest(`must include "rule" field in request body when action is "patch"`)
}
patchedRule, err := getInterfaceManager(c).InterfacesRequestsManager().PatchRule(userID, ruleID, postBody.PatchRule.Constraints, postBody.PatchRule.Outcome, postBody.PatchRule.Lifespan, postBody.PatchRule.Duration)
patchedRule, err := getInterfaceManager(c).InterfacesRequestsManager().PatchRule(userID, ruleID, postBody.PatchRule.Constraints)
if err != nil {
return promptingError(err)
}
Expand Down
184 changes: 105 additions & 79 deletions daemon/api_prompting_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,17 @@ type fakeInterfacesRequestsManager struct {
err error

// Store most recent received values
userID uint32
snap string
iface string
id prompting.IDType // used for prompt ID or rule ID
constraints *prompting.Constraints
outcome prompting.OutcomeType
lifespan prompting.LifespanType
duration string
clientActivity bool
userID uint32
snap string
iface string
id prompting.IDType // used for prompt ID or rule ID
ruleConstraints *prompting.Constraints
constraintsPatch *prompting.RuleConstraintsPatch
replyConstraints *prompting.ReplyConstraints
outcome prompting.OutcomeType
lifespan prompting.LifespanType
duration string
clientActivity bool
}

func (m *fakeInterfacesRequestsManager) Prompts(userID uint32, clientActivity bool) ([]*requestprompts.Prompt, error) {
Expand All @@ -76,10 +78,10 @@ func (m *fakeInterfacesRequestsManager) PromptWithID(userID uint32, promptID pro
return m.prompt, m.err
}

func (m *fakeInterfacesRequestsManager) HandleReply(userID uint32, promptID prompting.IDType, constraints *prompting.Constraints, outcome prompting.OutcomeType, lifespan prompting.LifespanType, duration string, clientActivity bool) ([]prompting.IDType, error) {
func (m *fakeInterfacesRequestsManager) HandleReply(userID uint32, promptID prompting.IDType, constraints *prompting.ReplyConstraints, outcome prompting.OutcomeType, lifespan prompting.LifespanType, duration string, clientActivity bool) ([]prompting.IDType, error) {
m.userID = userID
m.id = promptID
m.constraints = constraints
m.replyConstraints = constraints
m.outcome = outcome
m.lifespan = lifespan
m.duration = duration
Expand All @@ -94,14 +96,11 @@ func (m *fakeInterfacesRequestsManager) Rules(userID uint32, snap string, iface
return m.rules, m.err
}

func (m *fakeInterfacesRequestsManager) AddRule(userID uint32, snap string, iface string, constraints *prompting.Constraints, outcome prompting.OutcomeType, lifespan prompting.LifespanType, duration string) (*requestrules.Rule, error) {
func (m *fakeInterfacesRequestsManager) AddRule(userID uint32, snap string, iface string, constraints *prompting.Constraints) (*requestrules.Rule, error) {
m.userID = userID
m.snap = snap
m.iface = iface
m.constraints = constraints
m.outcome = outcome
m.lifespan = lifespan
m.duration = duration
m.ruleConstraints = constraints
return m.rule, m.err
}

Expand All @@ -118,13 +117,10 @@ func (m *fakeInterfacesRequestsManager) RuleWithID(userID uint32, ruleID prompti
return m.rule, m.err
}

func (m *fakeInterfacesRequestsManager) PatchRule(userID uint32, ruleID prompting.IDType, constraints *prompting.Constraints, outcome prompting.OutcomeType, lifespan prompting.LifespanType, duration string) (*requestrules.Rule, error) {
func (m *fakeInterfacesRequestsManager) PatchRule(userID uint32, ruleID prompting.IDType, constraintsPatch *prompting.RuleConstraintsPatch) (*requestrules.Rule, error) {
m.userID = userID
m.id = ruleID
m.constraints = constraints
m.outcome = outcome
m.lifespan = lifespan
m.duration = duration
m.constraintsPatch = constraintsPatch
return m.rule, m.err
}

Expand Down Expand Up @@ -706,7 +702,7 @@ func (s *promptingSuite) TestPostPromptHappy(c *C) {
prompting.IDType(0xF00BA4),
}

constraints := &prompting.Constraints{
constraints := &prompting.ReplyConstraints{
PathPattern: mustParsePathPattern(c, "/home/test/Pictures/**/*.{png,svg}"),
Permissions: []string{"read", "execute"},
}
Expand All @@ -724,7 +720,7 @@ func (s *promptingSuite) TestPostPromptHappy(c *C) {
// Check parameters
c.Check(s.manager.userID, Equals, uint32(1000))
c.Check(s.manager.id, Equals, prompting.IDType(0x0123456789abcdef))
c.Check(s.manager.constraints, DeepEquals, contents.Constraints)
c.Check(s.manager.replyConstraints, DeepEquals, contents.Constraints)
c.Check(s.manager.outcome, Equals, contents.Outcome)
c.Check(s.manager.lifespan, Equals, contents.Lifespan)
c.Check(s.manager.duration, Equals, contents.Duration)
Expand Down Expand Up @@ -782,13 +778,15 @@ func (s *promptingSuite) TestGetRulesHappy(c *C) {
User: 1234,
Snap: "firefox",
Interface: "home",
Constraints: &prompting.Constraints{
Constraints: &prompting.RuleConstraints{
PathPattern: mustParsePathPattern(c, "/foo/bar"),
Permissions: []string{"write"},
Permissions: prompting.RulePermissionMap{
"write": &prompting.RulePermissionEntry{
Outcome: prompting.OutcomeDeny,
Lifespan: prompting.LifespanForever,
},
},
},
Outcome: prompting.OutcomeDeny,
Lifespan: prompting.LifespanForever,
Expiration: time.Now(),
},
}

Expand Down Expand Up @@ -817,26 +815,34 @@ func (s *promptingSuite) TestPostRulesAddHappy(c *C) {
User: 11235,
Snap: "firefox",
Interface: "home",
Constraints: &prompting.Constraints{
Constraints: &prompting.RuleConstraints{
PathPattern: mustParsePathPattern(c, "/foo/bar/baz"),
Permissions: []string{"write"},
Permissions: prompting.RulePermissionMap{
"write": &prompting.RulePermissionEntry{
Outcome: prompting.OutcomeDeny,
Lifespan: prompting.LifespanForever,
},
},
},
Outcome: prompting.OutcomeDeny,
Lifespan: prompting.LifespanForever,
Expiration: time.Now(),
}

constraints := &prompting.Constraints{
PathPattern: mustParsePathPattern(c, "/home/test/{foo,bar,baz}/**/*.{png,svg}"),
Permissions: []string{"read", "write"},
Permissions: prompting.PermissionMap{
"read": &prompting.PermissionEntry{
Outcome: prompting.OutcomeAllow,
Lifespan: prompting.LifespanForever,
},
"write": &prompting.PermissionEntry{
Outcome: prompting.OutcomeAllow,
Lifespan: prompting.LifespanForever,
},
},
}
contents := &daemon.AddRuleContents{
Snap: "thunderbird",
Interface: "home",
Constraints: constraints,
Outcome: prompting.OutcomeAllow,
Lifespan: prompting.LifespanForever,
Duration: "",
}
postBody := &daemon.PostRulesRequestBody{
Action: "add",
Expand All @@ -851,10 +857,7 @@ func (s *promptingSuite) TestPostRulesAddHappy(c *C) {
c.Check(s.manager.userID, Equals, uint32(11235))
c.Check(s.manager.snap, Equals, contents.Snap)
c.Check(s.manager.iface, Equals, contents.Interface)
c.Check(s.manager.constraints, DeepEquals, contents.Constraints)
c.Check(s.manager.outcome, Equals, contents.Outcome)
c.Check(s.manager.lifespan, Equals, contents.Lifespan)
c.Check(s.manager.duration, Equals, contents.Duration)
c.Check(s.manager.ruleConstraints, DeepEquals, contents.Constraints)

// Check return value
rule, ok := rsp.Result.(*requestrules.Rule)
Expand Down Expand Up @@ -888,35 +891,42 @@ func (s *promptingSuite) TestPostRulesRemoveHappy(c *C) {
s.manager = &fakeInterfacesRequestsManager{}

// Set the rules to return
var timeZero time.Time
s.manager.rules = []*requestrules.Rule{
{
ID: prompting.IDType(1234),
Timestamp: time.Now(),
User: 1001,
Snap: "thunderird",
Interface: "home",
Constraints: &prompting.Constraints{
Constraints: &prompting.RuleConstraints{
PathPattern: mustParsePathPattern(c, "/foo/bar/baz/qux"),
Permissions: []string{"write"},
Permissions: prompting.RulePermissionMap{
"write": &prompting.RulePermissionEntry{
Outcome: prompting.OutcomeDeny,
Lifespan: prompting.LifespanForever,
},
},
},
Outcome: prompting.OutcomeDeny,
Lifespan: prompting.LifespanForever,
Expiration: timeZero,
},
{
ID: prompting.IDType(5678),
Timestamp: time.Now(),
User: 1001,
Snap: "thunderbird",
Interface: "home",
Constraints: &prompting.Constraints{
Constraints: &prompting.RuleConstraints{
PathPattern: mustParsePathPattern(c, "/fizz/buzz"),
Permissions: []string{"read", "execute"},
Permissions: prompting.RulePermissionMap{
"read": &prompting.RulePermissionEntry{
Outcome: prompting.OutcomeAllow,
Lifespan: prompting.LifespanTimespan,
},
"execute": &prompting.RulePermissionEntry{
Outcome: prompting.OutcomeAllow,
Lifespan: prompting.LifespanTimespan,
},
},
},
Outcome: prompting.OutcomeAllow,
Lifespan: prompting.LifespanTimespan,
Expiration: time.Now(),
},
}

Expand Down Expand Up @@ -955,13 +965,16 @@ func (s *promptingSuite) TestGetRuleHappy(c *C) {
User: 1005,
Snap: "thunderbird",
Interface: "home",
Constraints: &prompting.Constraints{
Constraints: &prompting.RuleConstraints{
PathPattern: mustParsePathPattern(c, "/home/test/Videos/**/*.{mkv,mp4,mov}"),
Permissions: []string{"read"},
Permissions: prompting.RulePermissionMap{
"read": &prompting.RulePermissionEntry{
Outcome: prompting.OutcomeAllow,
Lifespan: prompting.LifespanTimespan,
Expiration: time.Now().Add(-24 * time.Hour),
},
},
},
Outcome: prompting.OutcomeAllow,
Lifespan: prompting.LifespanTimespan,
Expiration: time.Now().Add(-24 * time.Hour),
}

rsp := s.makeSyncReq(c, "GET", "/v2/interfaces/requests/rules/000000000000012B", 1005, nil)
Expand All @@ -981,31 +994,42 @@ func (s *promptingSuite) TestPostRulePatchHappy(c *C) {

s.daemon(c)

var timeZero time.Time
s.manager.rule = &requestrules.Rule{
ID: prompting.IDType(0x01123581321),
Timestamp: time.Now(),
User: 999,
Snap: "gimp",
Interface: "home",
Constraints: &prompting.Constraints{
Constraints: &prompting.RuleConstraints{
PathPattern: mustParsePathPattern(c, "/home/test/Pictures/**/*.{png,jpg}"),
Permissions: []string{"read", "write"},
Permissions: prompting.RulePermissionMap{
"read": &prompting.RulePermissionEntry{
Outcome: prompting.OutcomeAllow,
Lifespan: prompting.LifespanForever,
},
"write": &prompting.RulePermissionEntry{
Outcome: prompting.OutcomeAllow,
Lifespan: prompting.LifespanForever,
},
},
},
Outcome: prompting.OutcomeAllow,
Lifespan: prompting.LifespanForever,
Expiration: timeZero,
}

constraints := &prompting.Constraints{
constraintsPatch := &prompting.RuleConstraintsPatch{
PathPattern: mustParsePathPattern(c, "/home/test/Pictures/**/*.{png,jpg}"),
Permissions: []string{"read", "write"},
Permissions: prompting.PermissionMap{
"read": &prompting.PermissionEntry{
Outcome: prompting.OutcomeAllow,
Lifespan: prompting.LifespanForever,
},
"write": &prompting.PermissionEntry{
Outcome: prompting.OutcomeAllow,
Lifespan: prompting.LifespanForever,
},
},
}
contents := &daemon.PatchRuleContents{
Constraints: constraints,
Outcome: prompting.OutcomeAllow,
Lifespan: prompting.LifespanForever,
Duration: "",
Constraints: constraintsPatch,
}
postBody := &daemon.PostRuleRequestBody{
Action: "patch",
Expand All @@ -1018,10 +1042,7 @@ func (s *promptingSuite) TestPostRulePatchHappy(c *C) {

// Check parameters
c.Check(s.manager.userID, Equals, uint32(999))
c.Check(s.manager.constraints, DeepEquals, contents.Constraints)
c.Check(s.manager.outcome, Equals, contents.Outcome)
c.Check(s.manager.lifespan, Equals, contents.Lifespan)
c.Check(s.manager.duration, Equals, contents.Duration)
c.Check(s.manager.constraintsPatch, DeepEquals, contents.Constraints)

// Check return value
rule, ok := rsp.Result.(*requestrules.Rule)
Expand All @@ -1034,20 +1055,25 @@ func (s *promptingSuite) TestPostRuleRemoveHappy(c *C) {

s.daemon(c)

var timeZero time.Time
s.manager.rule = &requestrules.Rule{
ID: prompting.IDType(0x01123581321),
Timestamp: time.Now(),
User: 100,
Snap: "gimp",
Interface: "home",
Constraints: &prompting.Constraints{
Constraints: &prompting.RuleConstraints{
PathPattern: mustParsePathPattern(c, "/home/test/Pictures/**/*.{png,jpg}"),
Permissions: []string{"read", "write"},
Permissions: prompting.RulePermissionMap{
"read": &prompting.RulePermissionEntry{
Outcome: prompting.OutcomeAllow,
Lifespan: prompting.LifespanForever,
},
"write": &prompting.RulePermissionEntry{
Outcome: prompting.OutcomeAllow,
Lifespan: prompting.LifespanForever,
},
},
},
Outcome: prompting.OutcomeAllow,
Lifespan: prompting.LifespanForever,
Expiration: timeZero,
}
postBody := &daemon.PostRuleRequestBody{
Action: "remove",
Expand Down
Loading
Loading