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 }