Skip to content

Commit

Permalink
i/prompting: add constraints and abstract permissions (snapcore#13850)
Browse files Browse the repository at this point in the history
This PR introduces constraints and abstract permissions.

* i/prompting: add constraints and abstract permissions

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

i/prompting: added function to select one interface

Multiple interfaces may be included in the tag in the kernel message,
and the listener passes these on to the other prompting components. This
PR adds a function to decide which of those interfaces to use in prompt
requests and rules.

Rules only apply to a particular interface, and we don't want duplicate
rules, so we must choose one interface from the list provided by the
listener which we use for the prompting requests and rules associated
with the listener request.

It is rather arbitrary which interfaces should have priority, and in
many cases interfaces do not have overlapping permissions, but we should
nonetheless manually assign a priority to any interface for which we
enable prompting.

Any request with only interfaces which are not explicitly included in
the list will be treated as having interface "other", as will any
request with an empty interfaces list.

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

i/prompting: add "constraints" field to rules and replies

Adds a "constraints" field to request rules and other related structure,
such as prompt replies. These constraints vary by interface, with some
interfaces supporting different permissions than others, some interfaces
supporting different constraints on path patterns (or non-path
resources), and possibly future extensions in the future. The idea
behind constraints is to allow these interface-specific variations in
the future.

Addionally, there are some changes to behavior which are introduced
alongside the constraints changes:
1. Constraints (formerly permissions lists) are no longer duplicated
   when creating rules, to avoid unnecessary memory allocations.
2. Permissions are removed from constraints (formerly permission lists)
   in-place, rather than by creating a new list, again to avoid
   unnecessary memory allocations, so constraints should never be shared
   or reused between multiple rules.
3. Prompt reply fields are validated *before* sending back a reply to
   the kernel, and if any fields are invalid, or the reply constraints
   do not match the original request, a reply is not sent.

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

i/prompting: abstract apparmor permissions

Convert AppArmor permissions into abstract permission strings, where the
available permissions are dependent on the interface associated with the
prompt or rule.

This allows greater flexibility to accept requests with new interfaces
and/or new mediation classes from the kernel without changing the
user-facing API (at least, regarding permissions), and with minimal
internal code changes.

In particular, the functions for parsing request permissions from
AppArmor are modular, and all that is required to add a new interface
with an existing mediation class is to add the mappings from abstract to
AppArmor permissions.

Additionally, reorganized and added more unit tests to increase coverage.

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

o/i/a/common: unexport unused exported function

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

i/prompting: small refactors and quote variables in error messages

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

i/prompting: mark constraints fields as omitempty

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

i/prompting: move constraints and abstract permissions to interfaces/prompting

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* i/prompting: remove SelectSingleInterface and references to camera interface

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* i/prompting: removed switches for handlers based on interface name

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* i/prompting: use *time.Time for expiration

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* i/prompting: simplify RemovePermission

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* i/prompting: renamed AbstractPermissionsFromList to ValidatePermissions

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* i/prompting: remove ValidateConstraintsOutcomeLifespan* functions

`ValidateConstraintsOutcomeLifespanExpiration` should be replaced by a
`Validate` method on the forthcoming `RequestRule` type, while
`ValidateConstraintsOutcomeLifespanDuration` should be unnecessary,
since validation of outcomes and lifespans will occur during
unmarshalling, and converting from duration to expiration should be done
explicitly when necessary.

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* i/prompting: assume file permissions in AbstractPermissions{To,From}AppArmorPermissions

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* i/prompting: adjust abstract permission error messages

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* i/prompting: use separate test suite for constraints

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

---------

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
  • Loading branch information
olivercalder authored and MiguelPires committed May 2, 2024
1 parent ee5e3d1 commit c0fc3c9
Show file tree
Hide file tree
Showing 3 changed files with 789 additions and 0 deletions.
217 changes: 217 additions & 0 deletions interfaces/prompting/constraints.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2024 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package prompting

import (
"errors"
"fmt"

"github.com/snapcore/snapd/sandbox/apparmor/notify"
"github.com/snapcore/snapd/strutil"
)

var (
ErrPermissionNotInList = errors.New("permission not found in permissions list")
ErrPermissionsListEmpty = errors.New("permissions list empty")
ErrUnrecognizedFilePermission = errors.New("file permissions mask contains unrecognized permission")
)

type Constraints struct {
PathPattern string `json:"path-pattern,omitempty"`
Permissions []string `json:"permissions,omitempty"`
}

// ValidateForInterface returns nil if the constraints are valid for the given
// interface, otherwise returns an error.
func (c *Constraints) ValidateForInterface(iface string) error {
// TODO: change to this once PR #13866 is merged:
// if err := ValidatePathPattern(c.PathPattern); err != nil {
// return err
// }
return c.validatePermissions(iface)
}

// validatePermissions checks that the permissions for the given constraints
// are valid for the given interface. If not, returns an error, otherwise
// ensures that the permissions are in the order in which they occur in the
// list of available permissions for that interface.
func (c *Constraints) validatePermissions(iface string) error {
availablePerms, ok := interfacePermissionsAvailable[iface]
if !ok {
return fmt.Errorf("unsupported interface: %s", iface)
}
permsSet := make(map[string]bool, len(c.Permissions))
for _, perm := range c.Permissions {
if !strutil.ListContains(availablePerms, perm) {
return fmt.Errorf("unsupported permission for %s interface: %q", iface, perm)
}
permsSet[perm] = true
}
if len(permsSet) == 0 {
return ErrPermissionsListEmpty
}
newPermissions := make([]string, 0, len(permsSet))
for _, perm := range availablePerms {
if exists := permsSet[perm]; exists {
newPermissions = append(newPermissions, perm)
}
}
c.Permissions = newPermissions
return nil
}

// Match returns true if the constraints match the given path, otherwise false.
//
// If the constraints or path are invalid, returns an error.
func (c *Constraints) Match(path string) (bool, error) {
// TODO: change to this once PR #13866 is merged:
// return PathPatternMatch(c.PathPattern, path)
return true, nil
}

// RemovePermission removes every instance of the given permission from the
// permissions list associated with the constraints. If the permission does
// not exist in the list, returns ErrPermissionNotInList.
func (c *Constraints) RemovePermission(permission string) error {
newPermissions := make([]string, 0, len(c.Permissions))
for _, perm := range c.Permissions {
if perm != permission {
newPermissions = append(newPermissions, perm)
}
}
if len(newPermissions) == len(c.Permissions) {
return ErrPermissionNotInList
}
c.Permissions = newPermissions
return nil
}

// ContainPermissions returns true if the constraints include every one of the
// given permissions.
func (c *Constraints) ContainPermissions(permissions []string) bool {
for _, perm := range permissions {
if !strutil.ListContains(c.Permissions, perm) {
return false
}
}
return true
}

var (
// List of permissions available for each interface. This also defines the
// order in which the permissions should be presented.
interfacePermissionsAvailable = map[string][]string{
"home": {"read", "write", "execute"},
}

// A mapping from interfaces which support AppArmor file permissions to
// the map between abstract permissions and those file permissions.
//
// Never include AA_MAY_OPEN in the maps below; it should always come from
// the kernel with another permission (e.g. AA_MAY_READ or AA_MAY_WRITE),
// and if it does not, it should be interpreted as AA_MAY_READ.
interfaceFilePermissionsMaps = map[string]map[string]notify.FilePermission{
"home": {
"read": notify.AA_MAY_READ,
"write": notify.AA_MAY_WRITE | notify.AA_MAY_APPEND | notify.AA_MAY_CREATE | notify.AA_MAY_DELETE | notify.AA_MAY_RENAME | notify.AA_MAY_CHMOD | notify.AA_MAY_LOCK | notify.AA_MAY_LINK,
"execute": notify.AA_MAY_EXEC | notify.AA_EXEC_MMAP,
},
}
)

// AvailablePermissions returns the list of available permissions for the given
// interface.
func AvailablePermissions(iface string) ([]string, error) {
available, exist := interfacePermissionsAvailable[iface]
if !exist {
return nil, fmt.Errorf("cannot get available permissions: unsupported interface: %s", iface)
}
return available, nil
}

// AbstractPermissionsFromAppArmorPermissions returns the list of permissions
// corresponding to the given AppArmor permissions for the given interface.
func AbstractPermissionsFromAppArmorPermissions(iface string, permissions interface{}) ([]string, error) {
filePerms, ok := permissions.(notify.FilePermission)
if !ok {
return nil, fmt.Errorf("cannot parse the given permissions as file permissions: %v", permissions)
}
if filePerms == notify.FilePermission(0) {
return nil, fmt.Errorf("cannot get abstract permissions from empty AppArmor permissions: %q", filePerms)
}
abstractPermsAvailable, exists := interfacePermissionsAvailable[iface]
if !exists {
return nil, fmt.Errorf("cannot map the given interface to list of available permissions: %s", iface)
}
abstractPermsMap, exists := interfaceFilePermissionsMaps[iface]
if !exists {
// This should never happen, since we just found a permissions list
// for the given interface and thus a map should exist for it as well.
return nil, fmt.Errorf("cannot map the given interface to map from abstract permissions to AppArmor permissions: %s", iface)
}
if filePerms == notify.AA_MAY_OPEN {
// Should not occur, but if a request is received for only open, treat it as read.
filePerms = notify.AA_MAY_READ
}
// Discard Open permission; re-add it to the permission mask later
filePerms &= ^notify.AA_MAY_OPEN
abstractPerms := make([]string, 0, 1) // most requests should only include one permission
for _, abstractPerm := range abstractPermsAvailable {
aaPermMapping, exists := abstractPermsMap[abstractPerm]
if !exists {
// This should never happen, since permission mappings are
// predefined and should be checked for correctness.
return nil, fmt.Errorf("internal error: cannot map abstract permission to AppArmor permissions for the %s interface: %q", iface, abstractPerm)
}
if filePerms&aaPermMapping != 0 {
abstractPerms = append(abstractPerms, abstractPerm)
filePerms &= ^aaPermMapping
}
}
if filePerms != notify.FilePermission(0) {
return nil, fmt.Errorf("cannot map AppArmor permission to abstract permission for the %s interface: %q", iface, filePerms)
}
return abstractPerms, nil
}

// AbstractPermissionsToAppArmorPermissions returns AppArmor permissions
// corresponding to the given permissions for the given interface.
func AbstractPermissionsToAppArmorPermissions(iface string, permissions []string) (interface{}, error) {
if len(permissions) == 0 {
return notify.FilePermission(0), ErrPermissionsListEmpty
}
filePermsMap, exists := interfaceFilePermissionsMaps[iface]
if !exists {
return notify.FilePermission(0), fmt.Errorf("cannot map the given interface to map from abstract permissions to AppArmor permissions: %s", iface)
}
filePerms := notify.FilePermission(0)
for _, perm := range permissions {
permMask, exists := filePermsMap[perm]
if !exists {
// Should not occur, since stored permissions list should have been validated
return notify.FilePermission(0), fmt.Errorf("cannot map abstract permission to AppArmor permissions for the %s interface: %q", iface, perm)
}
filePerms |= permMask
}
if filePerms&(notify.AA_MAY_EXEC|notify.AA_MAY_WRITE|notify.AA_MAY_READ|notify.AA_MAY_APPEND|notify.AA_MAY_CREATE) != 0 {
filePerms |= notify.AA_MAY_OPEN
}
return filePerms, nil
}

0 comments on commit c0fc3c9

Please sign in to comment.