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 }