gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/interfaces/builtin/content.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package builtin
    21  
    22  import (
    23  	"bytes"
    24  	"fmt"
    25  	"path/filepath"
    26  	"strings"
    27  
    28  	"github.com/snapcore/snapd/interfaces"
    29  	"github.com/snapcore/snapd/interfaces/apparmor"
    30  	"github.com/snapcore/snapd/interfaces/mount"
    31  	"github.com/snapcore/snapd/osutil"
    32  	"github.com/snapcore/snapd/snap"
    33  )
    34  
    35  const contentSummary = `allows sharing code and data with other snaps`
    36  
    37  const contentBaseDeclarationSlots = `
    38    content:
    39      allow-installation:
    40        slot-snap-type:
    41          - app
    42          - gadget
    43      allow-connection:
    44        plug-attributes:
    45          content: $SLOT(content)
    46      allow-auto-connection:
    47        plug-publisher-id:
    48          - $SLOT_PUBLISHER_ID
    49        plug-attributes:
    50          content: $SLOT(content)
    51  `
    52  
    53  // contentInterface allows sharing content between snaps
    54  type contentInterface struct{}
    55  
    56  func (iface *contentInterface) Name() string {
    57  	return "content"
    58  }
    59  
    60  func (iface *contentInterface) StaticInfo() interfaces.StaticInfo {
    61  	return interfaces.StaticInfo{
    62  		Summary:              contentSummary,
    63  		BaseDeclarationSlots: contentBaseDeclarationSlots,
    64  
    65  		AffectsPlugOnRefresh: true,
    66  	}
    67  }
    68  
    69  func cleanSubPath(path string) bool {
    70  	return filepath.Clean(path) == path && path != ".." && !strings.HasPrefix(path, "../")
    71  }
    72  
    73  func (iface *contentInterface) BeforePrepareSlot(slot *snap.SlotInfo) error {
    74  	content, ok := slot.Attrs["content"].(string)
    75  	if !ok || len(content) == 0 {
    76  		if slot.Attrs == nil {
    77  			slot.Attrs = make(map[string]interface{})
    78  		}
    79  		// content defaults to "slot" name if unspecified
    80  		slot.Attrs["content"] = slot.Name
    81  	}
    82  
    83  	// Error if "read" or "write" are present alongside "source".
    84  	// TODO: use slot.Lookup() once PR 4510 lands.
    85  	var unused map[string]interface{}
    86  	if err := slot.Attr("source", &unused); err == nil {
    87  		var unused []interface{}
    88  		if err := slot.Attr("read", &unused); err == nil {
    89  			return fmt.Errorf(`move the "read" attribute into the "source" section`)
    90  		}
    91  		if err := slot.Attr("write", &unused); err == nil {
    92  			return fmt.Errorf(`move the "write" attribute into the "source" section`)
    93  		}
    94  	}
    95  
    96  	// check that we have either a read or write path
    97  	rpath := iface.path(slot, "read")
    98  	wpath := iface.path(slot, "write")
    99  	if len(rpath) == 0 && len(wpath) == 0 {
   100  		return fmt.Errorf("read or write path must be set")
   101  	}
   102  
   103  	// go over both paths
   104  	paths := rpath
   105  	paths = append(paths, wpath...)
   106  	for _, p := range paths {
   107  		if !cleanSubPath(p) {
   108  			return fmt.Errorf("content interface path is not clean: %q", p)
   109  		}
   110  	}
   111  	return nil
   112  }
   113  
   114  func (iface *contentInterface) BeforePreparePlug(plug *snap.PlugInfo) error {
   115  	content, ok := plug.Attrs["content"].(string)
   116  	if !ok || len(content) == 0 {
   117  		if plug.Attrs == nil {
   118  			plug.Attrs = make(map[string]interface{})
   119  		}
   120  		// content defaults to "plug" name if unspecified
   121  		plug.Attrs["content"] = plug.Name
   122  	}
   123  	target, ok := plug.Attrs["target"].(string)
   124  	if !ok || len(target) == 0 {
   125  		return fmt.Errorf("content plug must contain target path")
   126  	}
   127  	if !cleanSubPath(target) {
   128  		return fmt.Errorf("content interface target path is not clean: %q", target)
   129  	}
   130  
   131  	return nil
   132  }
   133  
   134  // path is an internal helper that extract the "read" and "write" attribute
   135  // of the slot
   136  func (iface *contentInterface) path(attrs interfaces.Attrer, name string) []string {
   137  	if name != "read" && name != "write" {
   138  		panic("internal error, path can only be used with read/write")
   139  	}
   140  
   141  	var paths []interface{}
   142  	var source map[string]interface{}
   143  
   144  	if err := attrs.Attr("source", &source); err == nil {
   145  		// Access either "source.read" or "source.write" attribute.
   146  		var ok bool
   147  		if paths, ok = source[name].([]interface{}); !ok {
   148  			return nil
   149  		}
   150  	} else {
   151  		// Access either "read" or "write" attribute directly (legacy).
   152  		if err := attrs.Attr(name, &paths); err != nil {
   153  			return nil
   154  		}
   155  	}
   156  
   157  	out := make([]string, len(paths))
   158  	for i, p := range paths {
   159  		var ok bool
   160  		out[i], ok = p.(string)
   161  		if !ok {
   162  			return nil
   163  		}
   164  	}
   165  	return out
   166  }
   167  
   168  // resolveSpecialVariable resolves one of the three $SNAP* variables at the
   169  // beginning of a given path.  The variables are $SNAP, $SNAP_DATA and
   170  // $SNAP_COMMON. If there are no variables then $SNAP is implicitly assumed
   171  // (this is the behavior that was used before the variables were supporter).
   172  func resolveSpecialVariable(path string, snapInfo *snap.Info) string {
   173  	// Content cannot be mounted at arbitrary locations, validate the path
   174  	// for extra safety.
   175  	if err := snap.ValidatePathVariables(path); err == nil && strings.HasPrefix(path, "$") {
   176  		// The path starts with $ and ValidatePathVariables() ensures
   177  		// path contains only $SNAP, $SNAP_DATA, $SNAP_COMMON, and no
   178  		// other $VARs are present. It is ok to use
   179  		// ExpandSnapVariables() since it only expands $SNAP, $SNAP_DATA
   180  		// and $SNAP_COMMON
   181  		return snapInfo.ExpandSnapVariables(path)
   182  	}
   183  	// Always prefix with $SNAP if nothing else is provided or the path
   184  	// contains invalid variables.
   185  	return snapInfo.ExpandSnapVariables(filepath.Join("$SNAP", path))
   186  }
   187  
   188  func sourceTarget(plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot, relSrc string) (string, string) {
   189  	var target string
   190  	// The 'target' attribute has already been verified in BeforePreparePlug.
   191  	_ = plug.Attr("target", &target)
   192  	source := resolveSpecialVariable(relSrc, slot.Snap())
   193  	target = resolveSpecialVariable(target, plug.Snap())
   194  
   195  	// Check if the "source" section is present.
   196  	var unused map[string]interface{}
   197  	if err := slot.Attr("source", &unused); err == nil {
   198  		_, sourceName := filepath.Split(source)
   199  		target = filepath.Join(target, sourceName)
   200  	}
   201  	return source, target
   202  }
   203  
   204  func mountEntry(plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot, relSrc string, extraOptions ...string) osutil.MountEntry {
   205  	options := make([]string, 0, len(extraOptions)+1)
   206  	options = append(options, "bind")
   207  	options = append(options, extraOptions...)
   208  	source, target := sourceTarget(plug, slot, relSrc)
   209  	return osutil.MountEntry{
   210  		Name:    source,
   211  		Dir:     target,
   212  		Options: options,
   213  	}
   214  }
   215  
   216  func (iface *contentInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
   217  	contentSnippet := bytes.NewBuffer(nil)
   218  	writePaths := iface.path(slot, "write")
   219  	emit := spec.AddUpdateNSf
   220  	if len(writePaths) > 0 {
   221  		fmt.Fprintf(contentSnippet, `
   222  # In addition to the bind mount, add any AppArmor rules so that
   223  # snaps may directly access the slot implementation's files. Due
   224  # to a limitation in the kernel's LSM hooks for AF_UNIX, these
   225  # are needed for using named sockets within the exported
   226  # directory.
   227  `)
   228  		for i, w := range writePaths {
   229  			fmt.Fprintf(contentSnippet, "%s/** mrwklix,\n",
   230  				resolveSpecialVariable(w, slot.Snap()))
   231  			source, target := sourceTarget(plug, slot, w)
   232  			emit("  # Read-write content sharing %s -> %s (w#%d)\n", plug.Ref(), slot.Ref(), i)
   233  			emit("  mount options=(bind, rw) %s/ -> %s{,-[0-9]*}/,\n", source, target)
   234  			emit("  mount options=(rprivate) -> %s{,-[0-9]*}/,\n", target)
   235  			emit("  umount %s{,-[0-9]*}/,\n", target)
   236  			// TODO: The assumed prefix depth could be optimized to be more
   237  			// precise since content sharing can only take place in a fixed
   238  			// list of places with well-known paths (well, constrained set of
   239  			// paths). This can be done when the prefix is actually consumed.
   240  			apparmor.GenWritableProfile(emit, source, 1)
   241  			apparmor.GenWritableProfile(emit, target, 1)
   242  			apparmor.GenWritableProfile(emit, fmt.Sprintf("%s-[0-9]*", target), 1)
   243  		}
   244  	}
   245  
   246  	readPaths := iface.path(slot, "read")
   247  	if len(readPaths) > 0 {
   248  		fmt.Fprintf(contentSnippet, `
   249  # In addition to the bind mount, add any AppArmor rules so that
   250  # snaps may directly access the slot implementation's files
   251  # read-only.
   252  `)
   253  		for i, r := range readPaths {
   254  			fmt.Fprintf(contentSnippet, "%s/** mrkix,\n",
   255  				resolveSpecialVariable(r, slot.Snap()))
   256  
   257  			source, target := sourceTarget(plug, slot, r)
   258  			emit("  # Read-only content sharing %s -> %s (r#%d)\n", plug.Ref(), slot.Ref(), i)
   259  			emit("  mount options=(bind) %s/ -> %s{,-[0-9]*}/,\n", source, target)
   260  			emit("  remount options=(bind, ro) %s{,-[0-9]*}/,\n", target)
   261  			emit("  mount options=(rprivate) -> %s{,-[0-9]*}/,\n", target)
   262  			emit("  umount %s{,-[0-9]*}/,\n", target)
   263  			// Look at the TODO comment above.
   264  			apparmor.GenWritableProfile(emit, source, 1)
   265  			apparmor.GenWritableProfile(emit, target, 1)
   266  			apparmor.GenWritableProfile(emit, fmt.Sprintf("%s-[0-9]*", target), 1)
   267  		}
   268  	}
   269  
   270  	spec.AddSnippet(contentSnippet.String())
   271  	return nil
   272  }
   273  
   274  func (iface *contentInterface) AppArmorConnectedSlot(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
   275  	contentSnippet := bytes.NewBuffer(nil)
   276  	writePaths := iface.path(slot, "write")
   277  	if len(writePaths) > 0 {
   278  		fmt.Fprintf(contentSnippet, `
   279  # When the content interface is writable, allow this slot
   280  # implementation to access the slot's exported files at the plugging
   281  # snap's mountpoint to accommodate software where the plugging app
   282  # tells the slotting app about files to share.
   283  `)
   284  		for _, w := range writePaths {
   285  			_, target := sourceTarget(plug, slot, w)
   286  			fmt.Fprintf(contentSnippet, "%s/** mrwklix,\n",
   287  				target)
   288  		}
   289  	}
   290  
   291  	spec.AddSnippet(contentSnippet.String())
   292  	return nil
   293  }
   294  
   295  func (iface *contentInterface) AutoConnect(plug *snap.PlugInfo, slot *snap.SlotInfo) bool {
   296  	// allow what declarations allowed
   297  	return true
   298  }
   299  
   300  // Interactions with the mount backend.
   301  
   302  func (iface *contentInterface) MountConnectedPlug(spec *mount.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
   303  	for _, r := range iface.path(slot, "read") {
   304  		err := spec.AddMountEntry(mountEntry(plug, slot, r, "ro"))
   305  		if err != nil {
   306  			return err
   307  		}
   308  	}
   309  	for _, w := range iface.path(slot, "write") {
   310  		err := spec.AddMountEntry(mountEntry(plug, slot, w))
   311  		if err != nil {
   312  			return err
   313  		}
   314  	}
   315  	return nil
   316  }
   317  
   318  func init() {
   319  	registerIface(&contentInterface{})
   320  }