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