github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/interfaces/builtin/common_files.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2018 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/snap"
    31  )
    32  
    33  type commonFilesInterface struct {
    34  	commonInterface
    35  
    36  	apparmorHeader    string
    37  	extraPathValidate func(string) error
    38  }
    39  
    40  // filesAAPerm can either be files{Read,Write} and converted to a string
    41  // expands into the right apparmor permissions for the files interface.
    42  type filesAAPerm int
    43  
    44  const (
    45  	filesRead filesAAPerm = iota
    46  	filesWrite
    47  )
    48  
    49  func (a filesAAPerm) String() string {
    50  	switch a {
    51  	case filesRead:
    52  		return "rk" // [r]ead and loc[k]
    53  	case filesWrite:
    54  		return "rwkl" // [r]ead, [w]rite, loc[k] and [l]ink//
    55  	}
    56  	panic(fmt.Sprintf("invalid perm: %d", a))
    57  }
    58  
    59  func formatPath(ip interface{}) (string, error) {
    60  	p, ok := ip.(string)
    61  	if !ok {
    62  		return "", fmt.Errorf("%[1]v (%[1]T) is not a string", ip)
    63  	}
    64  	prefix := ""
    65  	// Note that the {personal,system}-files interface impose
    66  	// limitations on the $HOME usage - system-files forbids it,
    67  	// personal only allows starting with $HOME in the path.
    68  	if strings.Contains(p, "$HOME") {
    69  		p = strings.Replace(p, "$HOME", "@{HOME}", -1)
    70  		prefix = "owner "
    71  	}
    72  	p = filepath.Clean(p)
    73  	p += "{,/,/**}"
    74  
    75  	return fmt.Sprintf("%s%q", prefix, p), nil
    76  }
    77  
    78  func allowPathAccess(buf *bytes.Buffer, perm filesAAPerm, paths []interface{}) error {
    79  	for _, rawPath := range paths {
    80  		p, err := formatPath(rawPath)
    81  		if err != nil {
    82  			return err
    83  		}
    84  		fmt.Fprintf(buf, "%s %s,\n", p, perm)
    85  	}
    86  	return nil
    87  }
    88  
    89  func (iface *commonFilesInterface) validatePaths(attrName string, paths []interface{}) error {
    90  	for _, npp := range paths {
    91  		np, ok := npp.(string)
    92  		if !ok {
    93  			return fmt.Errorf("%q must be a list of strings", attrName)
    94  		}
    95  		if err := iface.validateSinglePath(np); err != nil {
    96  			return err
    97  		}
    98  	}
    99  	return nil
   100  }
   101  
   102  func (iface *commonFilesInterface) validateSinglePath(np string) error {
   103  	if strings.HasSuffix(np, "/") {
   104  		return fmt.Errorf(`%q cannot end with "/"`, np)
   105  	}
   106  	p := filepath.Clean(np)
   107  	if p != np {
   108  		return fmt.Errorf("cannot use %q: try %q", np, filepath.Clean(np))
   109  	}
   110  	if strings.Contains(p, "~") {
   111  		return fmt.Errorf(`%q cannot contain "~"`, p)
   112  	}
   113  	if err := apparmor.ValidateNoAppArmorRegexp(p); err != nil {
   114  		return err
   115  	}
   116  
   117  	// extraPathValidation must be implemented by the interface
   118  	// that build on top of the abstract commonFilesInterface
   119  	if iface.extraPathValidate == nil {
   120  		panic("extraPathValidate must be set when using the commonFilesInterface")
   121  	}
   122  	if err := iface.extraPathValidate(np); err != nil {
   123  		return err
   124  	}
   125  	return nil
   126  }
   127  
   128  func (iface *commonFilesInterface) BeforePreparePlug(plug *snap.PlugInfo) error {
   129  	hasValidAttr := false
   130  	for _, att := range []string{"read", "write"} {
   131  		if _, ok := plug.Attrs[att]; !ok {
   132  			continue
   133  		}
   134  		paths, ok := plug.Attrs[att].([]interface{})
   135  		if !ok {
   136  			return fmt.Errorf("cannot add %s plug: %q must be a list of strings", iface.name, att)
   137  		}
   138  		if err := iface.validatePaths(att, paths); err != nil {
   139  			return fmt.Errorf("cannot add %s plug: %s", iface.name, err)
   140  		}
   141  		hasValidAttr = true
   142  	}
   143  	if !hasValidAttr {
   144  		return fmt.Errorf(`cannot add %s plug: needs valid "read" or "write" attribute`, iface.name)
   145  	}
   146  
   147  	return nil
   148  }
   149  
   150  func (iface *commonFilesInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
   151  	var reads, writes []interface{}
   152  	_ = plug.Attr("read", &reads)
   153  	_ = plug.Attr("write", &writes)
   154  
   155  	errPrefix := fmt.Sprintf(`cannot connect plug %s: `, plug.Name())
   156  	buf := bytes.NewBufferString(iface.apparmorHeader)
   157  	if err := allowPathAccess(buf, filesRead, reads); err != nil {
   158  		return fmt.Errorf("%s%v", errPrefix, err)
   159  	}
   160  	if err := allowPathAccess(buf, filesWrite, writes); err != nil {
   161  		return fmt.Errorf("%s%v", errPrefix, err)
   162  	}
   163  	spec.AddSnippet(buf.String())
   164  
   165  	return nil
   166  }