Skip to content

Commit

Permalink
i/p/requestrules,o/i/apparmorprompting: move request matching logic i…
Browse files Browse the repository at this point in the history
…nto requestrules

Signed-off-by: Oliver Calder <[email protected]>
  • Loading branch information
olivercalder committed Nov 14, 2024
1 parent 46b5c71 commit d9d8469
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 55 deletions.
10 changes: 10 additions & 0 deletions interfaces/prompting/requestrules/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ package requestrules

import (
"time"

"github.com/snapcore/snapd/testutil"
)

var JoinInternalErrors = joinInternalErrors
Expand All @@ -30,3 +32,11 @@ type RulesDBJSON rulesDBJSON
func (rule *Rule) Validate(currTime time.Time) (expired bool, err error) {
return rule.validate(currTime)
}

func (rdb *RuleDB) IsPathPermAllowed(user uint32, snap string, iface string, path string, permission string) (bool, error) {
return rdb.isPathPermAllowed(user, snap, iface, path, permission)
}

func MockIsPathPermAllowed(f func(rdb *RuleDB, user uint32, snap string, iface string, path string, permission string) (bool, error)) func() {
return testutil.Mock(&isPathPermAllowedByRuleDB, f)
}
37 changes: 35 additions & 2 deletions interfaces/prompting/requestrules/requestrules.go
Original file line number Diff line number Diff line change
Expand Up @@ -728,10 +728,43 @@ func (rdb *RuleDB) makeNewRule(user uint32, snap string, iface string, constrain
return &newRule, nil
}

// IsPathAllowed checks whether the given path with the given permission is
// IsRequestAllowed checks whether a request with the given parameters is
// allowed or denied by existing rules.
//
// If any of the given permissions are allowed, they are returned as
// allowedPerms. If any permissions are denied, then returns anyDenied as true.
// If any of the given permissions were not matched by an existing rule, then
// they are returned as remainingPerms. If an error occurred, returns it.
func (rdb *RuleDB) IsRequestAllowed(user uint32, snap string, iface string, path string, permissions []string) (allowedPerms []string, anyDenied bool, remainingPerms []string, err error) {
allowedPerms = make([]string, 0, len(permissions))
remainingPerms = make([]string, 0, len(permissions))
var errs []error
for _, perm := range permissions {
allowed, err := isPathPermAllowedByRuleDB(rdb, user, snap, iface, path, perm)
switch {
case err == nil:
if allowed {
allowedPerms = append(allowedPerms, perm)
} else {
anyDenied = true
}
case errors.Is(err, prompting_errors.ErrNoMatchingRule):
remainingPerms = append(remainingPerms, perm)
default:
errs = append(errs, err)
}
}
return allowedPerms, anyDenied, remainingPerms, prompting_errors.Join(errs...)
}

var isPathPermAllowedByRuleDB = func(rdb *RuleDB, user uint32, snap string, iface string, path string, permission string) (bool, error) {
return rdb.isPathPermAllowed(user, snap, iface, path, permission)

Check warning on line 761 in interfaces/prompting/requestrules/requestrules.go

View check run for this annotation

Codecov / codecov/patch

interfaces/prompting/requestrules/requestrules.go#L760-L761

Added lines #L760 - L761 were not covered by tests
}

// isPathPermAllowed checks whether the given path with the given permission is
// allowed or denied by existing rules for the given user, snap, and interface.
// If no rule applies, returns prompting_errors.ErrNoMatchingRule.
func (rdb *RuleDB) IsPathAllowed(user uint32, snap string, iface string, path string, permission string) (bool, error) {
func (rdb *RuleDB) isPathPermAllowed(user uint32, snap string, iface string, path string, permission string) (bool, error) {
rdb.mutex.RLock()
defer rdb.mutex.RUnlock()
permissionMap, ok := rdb.permissionDBForUserSnapInterfacePermission(user, snap, iface, permission)
Expand Down
146 changes: 140 additions & 6 deletions interfaces/prompting/requestrules/requestrules_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -893,7 +893,141 @@ func (s *requestrulesSuite) TestAddRuleExpired(c *C) {
s.checkNewNotices(c, expectedNoticeInfo)
}

func (s *requestrulesSuite) TestIsPathAllowedSimple(c *C) {
type isPathPermAllowedResult struct {
allowed bool
err error
}

func (s *requestrulesSuite) TestIsRequestAllowed(c *C) {
rdb, err := requestrules.New(nil)
c.Assert(err, IsNil)
c.Assert(rdb, NotNil)

user := uint32(1234)
snap := "firefox"
iface := "powerful"
path := "/path/to/something"

for _, testCase := range []struct {
requestedPerms []string
permReturns map[string]isPathPermAllowedResult
allowedPerms []string
anyDenied bool
remainingPerms []string
errStr string
}{
{
requestedPerms: []string{"foo", "bar", "baz"},
permReturns: map[string]isPathPermAllowedResult{
"foo": {true, nil},
"bar": {true, nil},
"baz": {true, nil},
},
allowedPerms: []string{"foo", "bar", "baz"},
anyDenied: false,
remainingPerms: []string{},
errStr: "",
},
{
requestedPerms: []string{"foo", "bar", "baz"},
permReturns: map[string]isPathPermAllowedResult{
"foo": {true, prompting_errors.ErrNoMatchingRule},
"bar": {false, prompting_errors.ErrNoMatchingRule},
"baz": {true, prompting_errors.ErrNoMatchingRule},
},
allowedPerms: []string{},
anyDenied: false,
remainingPerms: []string{"foo", "bar", "baz"},
errStr: "",
},
{
requestedPerms: []string{"foo", "bar", "baz"},
permReturns: map[string]isPathPermAllowedResult{
"foo": {true, prompting_errors.ErrNoMatchingRule},
"bar": {true, nil},
"baz": {false, prompting_errors.ErrNoMatchingRule},
},
allowedPerms: []string{"bar"},
anyDenied: false,
remainingPerms: []string{"foo", "baz"},
errStr: "",
},
{
requestedPerms: []string{"foo", "bar", "baz"},
permReturns: map[string]isPathPermAllowedResult{
"foo": {false, nil},
"bar": {true, nil},
"baz": {false, prompting_errors.ErrNoMatchingRule},
},
allowedPerms: []string{"bar"},
anyDenied: true,
remainingPerms: []string{"baz"},
errStr: "",
},
{
requestedPerms: []string{"foo", "bar"},
permReturns: map[string]isPathPermAllowedResult{
"foo": {true, nil},
"bar": {true, nil},
"baz": {true, fmt.Errorf("baz")},
},
allowedPerms: []string{"foo", "bar"},
anyDenied: false,
remainingPerms: []string{},
errStr: "",
},
{
requestedPerms: []string{"foo", "bar", "baz"},
permReturns: map[string]isPathPermAllowedResult{
"foo": {true, fmt.Errorf("foo")},
"bar": {true, nil},
"baz": {true, nil},
},
allowedPerms: []string{"bar", "baz"},
anyDenied: false,
remainingPerms: []string{},
errStr: "foo",
},
{
requestedPerms: []string{"foo", "bar", "baz", "qux", "fizz", "buzz"},
permReturns: map[string]isPathPermAllowedResult{
"foo": {true, fmt.Errorf("foo")},
"bar": {true, nil},
"baz": {true, prompting_errors.ErrNoMatchingRule},
"qux": {false, fmt.Errorf("qux")},
"fizz": {false, nil},
"buzz": {false, prompting_errors.ErrNoMatchingRule},
},
allowedPerms: []string{"bar"},
anyDenied: true,
remainingPerms: []string{"baz", "buzz"},
errStr: "foo\nqux",
},
} {
restore := requestrules.MockIsPathPermAllowed(func(r *requestrules.RuleDB, u uint32, s string, i string, p string, perm string) (bool, error) {
c.Assert(r, Equals, rdb)
c.Assert(u, Equals, user)
c.Assert(s, Equals, snap)
c.Assert(i, Equals, iface)
c.Assert(p, Equals, path)
result := testCase.permReturns[perm]
return result.allowed, result.err
})
defer restore()

allowedPerms, anyDenied, remainingPerms, err := rdb.IsRequestAllowed(user, snap, iface, path, testCase.requestedPerms)
c.Check(allowedPerms, DeepEquals, testCase.allowedPerms)
c.Check(anyDenied, Equals, testCase.anyDenied)
c.Check(remainingPerms, DeepEquals, testCase.remainingPerms)
if testCase.errStr == "" {
c.Check(err, IsNil)
} else {
c.Check(err, ErrorMatches, testCase.errStr)
}
}
}

func (s *requestrulesSuite) TestIsPathPermAllowedSimple(c *C) {
// Target
user := s.defaultUser
snap := "firefox"
Expand Down Expand Up @@ -976,7 +1110,7 @@ func (s *requestrulesSuite) TestIsPathAllowedSimple(c *C) {
s.checkNewNoticesSimple(c, nil)
}

allowed, err := rdb.IsPathAllowed(user, snap, iface, path, permission)
allowed, err := rdb.IsPathPermAllowed(user, snap, iface, path, permission)
c.Check(err, Equals, testCase.err)
c.Check(allowed, Equals, testCase.allowed)
// Check that no notices were recorded when checking
Expand All @@ -990,7 +1124,7 @@ func (s *requestrulesSuite) TestIsPathAllowedSimple(c *C) {
}
}

func (s *requestrulesSuite) TestIsPathAllowedPrecedence(c *C) {
func (s *requestrulesSuite) TestIsPathPermAllowedPrecedence(c *C) {
// Target
user := s.defaultUser
snap := "firefox"
Expand Down Expand Up @@ -1047,13 +1181,13 @@ func (s *requestrulesSuite) TestIsPathAllowedPrecedence(c *C) {
mostRecentOutcome, err := ruleContents.Outcome.AsBool()
c.Check(err, IsNil)

allowed, err := rdb.IsPathAllowed(user, snap, iface, path, permission)
allowed, err := rdb.IsPathPermAllowed(user, snap, iface, path, permission)
c.Check(err, IsNil)
c.Check(allowed, Equals, mostRecentOutcome, Commentf("most recent: %+v", ruleContents))
}
}

func (s *requestrulesSuite) TestIsPathAllowedExpiration(c *C) {
func (s *requestrulesSuite) TestIsPathPermAllowedExpiration(c *C) {
// Target
user := s.defaultUser
snap := "firefox"
Expand Down Expand Up @@ -1116,7 +1250,7 @@ func (s *requestrulesSuite) TestIsPathAllowedExpiration(c *C) {
c.Check(err, IsNil)

// Check that the outcome of the most specific unexpired rule has precedence
allowed, err := rdb.IsPathAllowed(user, snap, iface, path, permission)
allowed, err := rdb.IsPathPermAllowed(user, snap, iface, path, permission)
c.Check(err, IsNil)
c.Check(allowed, Equals, expectedOutcome, Commentf("last unexpired: %+v", rule))

Expand Down
65 changes: 18 additions & 47 deletions overlord/ifacestate/apparmorprompting/prompting.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
package apparmorprompting

import (
"errors"
"fmt"
"sync"

Expand Down Expand Up @@ -211,54 +210,26 @@ func (m *InterfacesRequestsManager) handleListenerReq(req *listener.Request) err
return requestReply(req, nil)
}

remainingPerms := make([]string, 0, len(permissions))
satisfiedPerms := make([]string, 0, len(permissions))

// we're done with early checks, serious business starts now, and we can
// take the lock
m.lock.Lock()
defer m.lock.Unlock()

matchedDenyRule := false
for _, perm := range permissions {
// TODO: move this for-loop to a helper in requestrules
if yesNo, err := m.rules.IsPathAllowed(userID, snap, iface, path, perm); err == nil {
if !yesNo {
matchedDenyRule = true
} else {
satisfiedPerms = append(satisfiedPerms, perm)
}
} else {
if !errors.Is(err, prompting_errors.ErrNoMatchingRule) {
logger.Noticef("error while checking request against existing rules: %v", err)
}
// No matching rule found
remainingPerms = append(remainingPerms, perm)
allowedPerms, matchedDenyRule, remainingPerms, err := m.rules.IsRequestAllowed(userID, snap, iface, path, permissions)
if err != nil || matchedDenyRule || len(remainingPerms) == 0 {
switch {
case err != nil:
logger.Noticef("error while checking request against existing rules: %v", err)

Check warning on line 222 in overlord/ifacestate/apparmorprompting/prompting.go

View check run for this annotation

Codecov / codecov/patch

overlord/ifacestate/apparmorprompting/prompting.go#L221-L222

Added lines #L221 - L222 were not covered by tests
case matchedDenyRule:
logger.Debugf("request denied by existing rule: %+v", req)
case len(remainingPerms) == 0:
logger.Debugf("request allowed by existing rule: %+v", req)
}
}
if matchedDenyRule {
logger.Debugf("request denied by existing rule: %+v", req)

// Respond with this information by allowing any requested permissions
// which were explicitly allowed by existing rules (there may be no
// such permissions) and let the listener deny all permissions which
// were not explicitly included in the allowed permissions.
allowedPermission, _ := prompting.AbstractPermissionsToAppArmorPermissions(iface, satisfiedPerms)
// Error should not occur, but if it does, allowedPermission is set to
// empty, leaving it to the listener to default deny all permissions.
return requestReply(req, allowedPermission)
}

if len(remainingPerms) == 0 {
logger.Debugf("request allowed by existing rule: %+v", req)

// We don't want to just send back req.Permission() here, since that
// could include unrecognized permissions which were discarded, and
// were not matched by an existing rule. So only respond with the
// permissions which were matched and allowed, the listener will
// deny any permissions which were not explicitly included in the
// allowed permissions.
allowedPermission, _ := prompting.AbstractPermissionsToAppArmorPermissions(iface, satisfiedPerms)
// Allow any requested permissions which were explicitly allowed by
// existing rules (there may be no such permissions) and let the
// listener deny all permissions which were not explicitly included in
// the allowed permissions.
allowedPermission, _ := prompting.AbstractPermissionsToAppArmorPermissions(iface, allowedPerms)
// Error should not occur, but if it does, allowedPermission is set to
// empty, leaving it to the listener to default deny all permissions.
return requestReply(req, allowedPermission)
Expand All @@ -279,18 +250,18 @@ func (m *InterfacesRequestsManager) handleListenerReq(req *listener.Request) err
// We weren't able to create a new prompt, so respond with the best
// information we have, which is to allow any permissions which were
// allowed by existing rules, and let the listener deny the rest.
allowedPermission, _ := prompting.AbstractPermissionsToAppArmorPermissions(iface, satisfiedPerms)
allowedPermission, _ := prompting.AbstractPermissionsToAppArmorPermissions(iface, allowedPerms)
// Error should not occur, but if it does, allowedPermission is set to
// empty, leaving it to the listener to default deny all permissions.
return requestReply(req, allowedPermission)
}

if merged {
logger.Debugf("new prompt merged with identical existing prompt: %+v", newPrompt)
return nil
} else {
logger.Debugf("adding prompt to internal storage: %+v", newPrompt)
}

logger.Debugf("adding prompt to internal storage: %+v", newPrompt)

return nil
}

Expand Down

0 comments on commit d9d8469

Please sign in to comment.