github.com/bugraaydogar/snapd@v0.0.0-20210315170335-8c70bb858939/interfaces/builtin/content_test.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_test 21 22 import ( 23 "path/filepath" 24 "strings" 25 26 . "gopkg.in/check.v1" 27 28 "github.com/snapcore/snapd/dirs" 29 "github.com/snapcore/snapd/interfaces" 30 "github.com/snapcore/snapd/interfaces/apparmor" 31 "github.com/snapcore/snapd/interfaces/builtin" 32 "github.com/snapcore/snapd/interfaces/mount" 33 "github.com/snapcore/snapd/osutil" 34 "github.com/snapcore/snapd/snap" 35 "github.com/snapcore/snapd/snap/snaptest" 36 "github.com/snapcore/snapd/testutil" 37 ) 38 39 type ContentSuite struct { 40 iface interfaces.Interface 41 } 42 43 var _ = Suite(&ContentSuite{ 44 iface: builtin.MustInterface("content"), 45 }) 46 47 func (s *ContentSuite) TestName(c *C) { 48 c.Assert(s.iface.Name(), Equals, "content") 49 } 50 51 func (s *ContentSuite) TestSanitizeSlotSimple(c *C) { 52 const mockSnapYaml = `name: content-slot-snap 53 version: 1.0 54 slots: 55 content-slot: 56 interface: content 57 content: mycont 58 read: 59 - shared/read 60 ` 61 info := snaptest.MockInfo(c, mockSnapYaml, nil) 62 slot := info.Slots["content-slot"] 63 c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), IsNil) 64 } 65 66 func (s *ContentSuite) TestSanitizeSlotContentLabelDefault(c *C) { 67 const mockSnapYaml = `name: content-slot-snap 68 version: 1.0 69 slots: 70 content-slot: 71 interface: content 72 read: 73 - shared/read 74 ` 75 info := snaptest.MockInfo(c, mockSnapYaml, nil) 76 slot := info.Slots["content-slot"] 77 c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), IsNil) 78 c.Assert(slot.Attrs["content"], Equals, slot.Name) 79 } 80 81 func (s *ContentSuite) TestSanitizeSlotNoPaths(c *C) { 82 const mockSnapYaml = `name: content-slot-snap 83 version: 1.0 84 slots: 85 content-slot: 86 interface: content 87 content: mycont 88 ` 89 info := snaptest.MockInfo(c, mockSnapYaml, nil) 90 slot := info.Slots["content-slot"] 91 c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches, "read or write path must be set") 92 } 93 94 func (s *ContentSuite) TestSanitizeSlotEmptyPaths(c *C) { 95 const mockSnapYaml = `name: content-slot-snap 96 version: 1.0 97 slots: 98 content-slot: 99 interface: content 100 content: mycont 101 read: [] 102 write: [] 103 ` 104 info := snaptest.MockInfo(c, mockSnapYaml, nil) 105 slot := info.Slots["content-slot"] 106 c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches, "read or write path must be set") 107 } 108 109 func (s *ContentSuite) TestSanitizeSlotHasRelativePath(c *C) { 110 const mockSnapYaml = `name: content-slot-snap 111 version: 1.0 112 slots: 113 content-slot: 114 interface: content 115 content: mycont 116 ` 117 for _, rw := range []string{"read: [../foo]", "write: [../bar]"} { 118 info := snaptest.MockInfo(c, mockSnapYaml+" "+rw, nil) 119 slot := info.Slots["content-slot"] 120 c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches, "content interface path is not clean:.*") 121 } 122 } 123 124 func (s *ContentSuite) TestSanitizeSlotSourceAndLegacy(c *C) { 125 slot := MockSlot(c, `name: snap 126 version: 0 127 slots: 128 content: 129 source: 130 write: [$SNAP_DATA/stuff] 131 read: [$SNAP/shared] 132 `, nil, "content") 133 c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches, `move the "read" attribute into the "source" section`) 134 slot = MockSlot(c, `name: snap 135 version: 0 136 slots: 137 content: 138 source: 139 read: [$SNAP/shared] 140 write: [$SNAP_DATA/stuff] 141 `, nil, "content") 142 c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches, `move the "write" attribute into the "source" section`) 143 } 144 145 func (s *ContentSuite) TestSanitizePlugSimple(c *C) { 146 const mockSnapYaml = `name: content-slot-snap 147 version: 1.0 148 plugs: 149 content-plug: 150 interface: content 151 content: mycont 152 target: import 153 ` 154 info := snaptest.MockInfo(c, mockSnapYaml, nil) 155 plug := info.Plugs["content-plug"] 156 c.Assert(interfaces.BeforePreparePlug(s.iface, plug), IsNil) 157 } 158 159 func (s *ContentSuite) TestSanitizePlugContentLabelDefault(c *C) { 160 const mockSnapYaml = `name: content-slot-snap 161 version: 1.0 162 plugs: 163 content-plug: 164 interface: content 165 target: import 166 ` 167 info := snaptest.MockInfo(c, mockSnapYaml, nil) 168 plug := info.Plugs["content-plug"] 169 c.Assert(interfaces.BeforePreparePlug(s.iface, plug), IsNil) 170 c.Assert(plug.Attrs["content"], Equals, plug.Name) 171 } 172 173 func (s *ContentSuite) TestSanitizePlugSimpleNoTarget(c *C) { 174 const mockSnapYaml = `name: content-slot-snap 175 version: 1.0 176 plugs: 177 content-plug: 178 interface: content 179 content: mycont 180 ` 181 info := snaptest.MockInfo(c, mockSnapYaml, nil) 182 plug := info.Plugs["content-plug"] 183 c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, "content plug must contain target path") 184 } 185 186 func (s *ContentSuite) TestSanitizePlugSimpleTargetRelative(c *C) { 187 const mockSnapYaml = `name: content-slot-snap 188 version: 1.0 189 plugs: 190 content-plug: 191 interface: content 192 content: mycont 193 target: ../foo 194 ` 195 info := snaptest.MockInfo(c, mockSnapYaml, nil) 196 plug := info.Plugs["content-plug"] 197 c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, "content interface target path is not clean:.*") 198 } 199 200 func (s *ContentSuite) TestSanitizePlugNilAttrMap(c *C) { 201 const mockSnapYaml = `name: content-slot-snap 202 version: 1.0 203 apps: 204 foo: 205 command: foo 206 plugs: [content] 207 ` 208 info := snaptest.MockInfo(c, mockSnapYaml, nil) 209 plug := info.Plugs["content"] 210 c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, "content plug must contain target path") 211 } 212 213 func (s *ContentSuite) TestSanitizeSlotNilAttrMap(c *C) { 214 const mockSnapYaml = `name: content-slot-snap 215 version: 1.0 216 apps: 217 foo: 218 command: foo 219 slots: [content] 220 ` 221 info := snaptest.MockInfo(c, mockSnapYaml, nil) 222 slot := info.Slots["content"] 223 c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches, "read or write path must be set") 224 } 225 226 func (s *ContentSuite) TestResolveSpecialVariable(c *C) { 227 info := snaptest.MockInfo(c, "{name: name, version: 0}", &snap.SideInfo{Revision: snap.R(42)}) 228 c.Check(builtin.ResolveSpecialVariable("$SNAP/foo", info), Equals, filepath.Join(dirs.CoreSnapMountDir, "name/42/foo")) 229 c.Check(builtin.ResolveSpecialVariable("$SNAP_DATA/foo", info), Equals, "/var/snap/name/42/foo") 230 c.Check(builtin.ResolveSpecialVariable("$SNAP_COMMON/foo", info), Equals, "/var/snap/name/common/foo") 231 c.Check(builtin.ResolveSpecialVariable("$SNAP", info), Equals, filepath.Join(dirs.CoreSnapMountDir, "name/42")) 232 c.Check(builtin.ResolveSpecialVariable("$SNAP_DATA", info), Equals, "/var/snap/name/42") 233 c.Check(builtin.ResolveSpecialVariable("$SNAP_COMMON", info), Equals, "/var/snap/name/common") 234 c.Check(builtin.ResolveSpecialVariable("$SNAP_DATA/", info), Equals, "/var/snap/name/42/") 235 // automatically prefixed with $SNAP 236 c.Check(builtin.ResolveSpecialVariable("foo", info), Equals, filepath.Join(dirs.CoreSnapMountDir, "name/42/foo")) 237 c.Check(builtin.ResolveSpecialVariable("foo/snap/bar", info), Equals, "/snap/name/42/foo/snap/bar") 238 // contain invalid variables 239 c.Check(builtin.ResolveSpecialVariable("$PRUNE/bar", info), Equals, "/snap/name/42//bar") 240 c.Check(builtin.ResolveSpecialVariable("bar/$PRUNE/foo", info), Equals, "/snap/name/42/bar//foo") 241 } 242 243 // Check that legacy syntax works and allows sharing read-only snap content 244 func (s *ContentSuite) TestConnectedPlugSnippetSharingLegacy(c *C) { 245 const consumerYaml = `name: consumer 246 version: 0 247 plugs: 248 content: 249 target: import 250 ` 251 consumerInfo := snaptest.MockInfo(c, consumerYaml, &snap.SideInfo{Revision: snap.R(7)}) 252 plug := interfaces.NewConnectedPlug(consumerInfo.Plugs["content"], nil, nil) 253 const producerYaml = `name: producer 254 version: 0 255 slots: 256 content: 257 read: 258 - export 259 ` 260 producerInfo := snaptest.MockInfo(c, producerYaml, &snap.SideInfo{Revision: snap.R(5)}) 261 slot := interfaces.NewConnectedSlot(producerInfo.Slots["content"], nil, nil) 262 263 spec := &mount.Specification{} 264 c.Assert(spec.AddConnectedPlug(s.iface, plug, slot), IsNil) 265 expectedMnt := []osutil.MountEntry{{ 266 Name: filepath.Join(dirs.CoreSnapMountDir, "producer/5/export"), 267 Dir: filepath.Join(dirs.CoreSnapMountDir, "consumer/7/import"), 268 Options: []string{"bind", "ro"}, 269 }} 270 c.Assert(spec.MountEntries(), DeepEquals, expectedMnt) 271 } 272 273 // Check that sharing of read-only snap content is possible 274 func (s *ContentSuite) TestConnectedPlugSnippetSharingSnap(c *C) { 275 const consumerYaml = `name: consumer 276 version: 0 277 plugs: 278 content: 279 target: $SNAP/import 280 apps: 281 app: 282 command: foo 283 ` 284 consumerInfo := snaptest.MockInfo(c, consumerYaml, &snap.SideInfo{Revision: snap.R(7)}) 285 plug := interfaces.NewConnectedPlug(consumerInfo.Plugs["content"], nil, nil) 286 const producerYaml = `name: producer 287 version: 0 288 slots: 289 content: 290 read: 291 - $SNAP/export 292 ` 293 producerInfo := snaptest.MockInfo(c, producerYaml, &snap.SideInfo{Revision: snap.R(5)}) 294 slot := interfaces.NewConnectedSlot(producerInfo.Slots["content"], nil, nil) 295 296 spec := &mount.Specification{} 297 c.Assert(spec.AddConnectedPlug(s.iface, plug, slot), IsNil) 298 expectedMnt := []osutil.MountEntry{{ 299 Name: filepath.Join(dirs.CoreSnapMountDir, "producer/5/export"), 300 Dir: filepath.Join(dirs.CoreSnapMountDir, "consumer/7/import"), 301 Options: []string{"bind", "ro"}, 302 }} 303 c.Assert(spec.MountEntries(), DeepEquals, expectedMnt) 304 305 apparmorSpec := &apparmor.Specification{} 306 err := apparmorSpec.AddConnectedPlug(s.iface, plug, slot) 307 c.Assert(err, IsNil) 308 c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) 309 expected := ` 310 # In addition to the bind mount, add any AppArmor rules so that 311 # snaps may directly access the slot implementation's files 312 # read-only. 313 /snap/producer/5/export/** mrkix, 314 ` 315 c.Assert(apparmorSpec.SnippetForTag("snap.consumer.app"), Equals, expected) 316 317 updateNS := apparmorSpec.UpdateNS() 318 profile0 := ` # Read-only content sharing consumer:content -> producer:content (r#0) 319 mount options=(bind) /snap/producer/5/export/ -> /snap/consumer/7/import{,-[0-9]*}/, 320 remount options=(bind, ro) /snap/consumer/7/import{,-[0-9]*}/, 321 mount options=(rprivate) -> /snap/consumer/7/import{,-[0-9]*}/, 322 umount /snap/consumer/7/import{,-[0-9]*}/, 323 # Writable mimic /snap/producer/5 324 # .. permissions for traversing the prefix that is assumed to exist 325 # .. variant with mimic at / 326 # Allow reading the mimic directory, it must exist in the first place. 327 / r, 328 # Allow setting the read-only directory aside via a bind mount. 329 /tmp/.snap/ rw, 330 mount options=(rbind, rw) / -> /tmp/.snap/, 331 # Allow mounting tmpfs over the read-only directory. 332 mount fstype=tmpfs options=(rw) tmpfs -> /, 333 # Allow creating empty files and directories for bind mounting things 334 # to reconstruct the now-writable parent directory. 335 /tmp/.snap/*/ rw, 336 /*/ rw, 337 mount options=(rbind, rw) /tmp/.snap/*/ -> /*/, 338 /tmp/.snap/* rw, 339 /* rw, 340 mount options=(bind, rw) /tmp/.snap/* -> /*, 341 # Allow unmounting the auxiliary directory. 342 # TODO: use fstype=tmpfs here for more strictness (LP: #1613403) 343 mount options=(rprivate) -> /tmp/.snap/, 344 umount /tmp/.snap/, 345 # Allow unmounting the destination directory as well as anything 346 # inside. This lets us perform the undo plan in case the writable 347 # mimic fails. 348 mount options=(rprivate) -> /, 349 mount options=(rprivate) -> /*, 350 mount options=(rprivate) -> /*/, 351 umount /, 352 umount /*, 353 umount /*/, 354 # .. variant with mimic at /snap/ 355 /snap/ r, 356 /tmp/.snap/snap/ rw, 357 mount options=(rbind, rw) /snap/ -> /tmp/.snap/snap/, 358 mount fstype=tmpfs options=(rw) tmpfs -> /snap/, 359 /tmp/.snap/snap/*/ rw, 360 /snap/*/ rw, 361 mount options=(rbind, rw) /tmp/.snap/snap/*/ -> /snap/*/, 362 /tmp/.snap/snap/* rw, 363 /snap/* rw, 364 mount options=(bind, rw) /tmp/.snap/snap/* -> /snap/*, 365 mount options=(rprivate) -> /tmp/.snap/snap/, 366 umount /tmp/.snap/snap/, 367 mount options=(rprivate) -> /snap/, 368 mount options=(rprivate) -> /snap/*, 369 mount options=(rprivate) -> /snap/*/, 370 umount /snap/, 371 umount /snap/*, 372 umount /snap/*/, 373 # .. variant with mimic at /snap/producer/ 374 /snap/producer/ r, 375 /tmp/.snap/snap/producer/ rw, 376 mount options=(rbind, rw) /snap/producer/ -> /tmp/.snap/snap/producer/, 377 mount fstype=tmpfs options=(rw) tmpfs -> /snap/producer/, 378 /tmp/.snap/snap/producer/*/ rw, 379 /snap/producer/*/ rw, 380 mount options=(rbind, rw) /tmp/.snap/snap/producer/*/ -> /snap/producer/*/, 381 /tmp/.snap/snap/producer/* rw, 382 /snap/producer/* rw, 383 mount options=(bind, rw) /tmp/.snap/snap/producer/* -> /snap/producer/*, 384 mount options=(rprivate) -> /tmp/.snap/snap/producer/, 385 umount /tmp/.snap/snap/producer/, 386 mount options=(rprivate) -> /snap/producer/, 387 mount options=(rprivate) -> /snap/producer/*, 388 mount options=(rprivate) -> /snap/producer/*/, 389 umount /snap/producer/, 390 umount /snap/producer/*, 391 umount /snap/producer/*/, 392 # .. variant with mimic at /snap/producer/5/ 393 /snap/producer/5/ r, 394 /tmp/.snap/snap/producer/5/ rw, 395 mount options=(rbind, rw) /snap/producer/5/ -> /tmp/.snap/snap/producer/5/, 396 mount fstype=tmpfs options=(rw) tmpfs -> /snap/producer/5/, 397 /tmp/.snap/snap/producer/5/*/ rw, 398 /snap/producer/5/*/ rw, 399 mount options=(rbind, rw) /tmp/.snap/snap/producer/5/*/ -> /snap/producer/5/*/, 400 /tmp/.snap/snap/producer/5/* rw, 401 /snap/producer/5/* rw, 402 mount options=(bind, rw) /tmp/.snap/snap/producer/5/* -> /snap/producer/5/*, 403 mount options=(rprivate) -> /tmp/.snap/snap/producer/5/, 404 umount /tmp/.snap/snap/producer/5/, 405 mount options=(rprivate) -> /snap/producer/5/, 406 mount options=(rprivate) -> /snap/producer/5/*, 407 mount options=(rprivate) -> /snap/producer/5/*/, 408 umount /snap/producer/5/, 409 umount /snap/producer/5/*, 410 umount /snap/producer/5/*/, 411 # Writable mimic /snap/consumer/7 412 # .. variant with mimic at /snap/consumer/ 413 /snap/consumer/ r, 414 /tmp/.snap/snap/consumer/ rw, 415 mount options=(rbind, rw) /snap/consumer/ -> /tmp/.snap/snap/consumer/, 416 mount fstype=tmpfs options=(rw) tmpfs -> /snap/consumer/, 417 /tmp/.snap/snap/consumer/*/ rw, 418 /snap/consumer/*/ rw, 419 mount options=(rbind, rw) /tmp/.snap/snap/consumer/*/ -> /snap/consumer/*/, 420 /tmp/.snap/snap/consumer/* rw, 421 /snap/consumer/* rw, 422 mount options=(bind, rw) /tmp/.snap/snap/consumer/* -> /snap/consumer/*, 423 mount options=(rprivate) -> /tmp/.snap/snap/consumer/, 424 umount /tmp/.snap/snap/consumer/, 425 mount options=(rprivate) -> /snap/consumer/, 426 mount options=(rprivate) -> /snap/consumer/*, 427 mount options=(rprivate) -> /snap/consumer/*/, 428 umount /snap/consumer/, 429 umount /snap/consumer/*, 430 umount /snap/consumer/*/, 431 # .. variant with mimic at /snap/consumer/7/ 432 /snap/consumer/7/ r, 433 /tmp/.snap/snap/consumer/7/ rw, 434 mount options=(rbind, rw) /snap/consumer/7/ -> /tmp/.snap/snap/consumer/7/, 435 mount fstype=tmpfs options=(rw) tmpfs -> /snap/consumer/7/, 436 /tmp/.snap/snap/consumer/7/*/ rw, 437 /snap/consumer/7/*/ rw, 438 mount options=(rbind, rw) /tmp/.snap/snap/consumer/7/*/ -> /snap/consumer/7/*/, 439 /tmp/.snap/snap/consumer/7/* rw, 440 /snap/consumer/7/* rw, 441 mount options=(bind, rw) /tmp/.snap/snap/consumer/7/* -> /snap/consumer/7/*, 442 mount options=(rprivate) -> /tmp/.snap/snap/consumer/7/, 443 umount /tmp/.snap/snap/consumer/7/, 444 mount options=(rprivate) -> /snap/consumer/7/, 445 mount options=(rprivate) -> /snap/consumer/7/*, 446 mount options=(rprivate) -> /snap/consumer/7/*/, 447 umount /snap/consumer/7/, 448 umount /snap/consumer/7/*, 449 umount /snap/consumer/7/*/, 450 ` 451 c.Assert(strings.Join(updateNS[:], ""), Equals, profile0) 452 } 453 454 // Check that sharing of writable data is possible 455 func (s *ContentSuite) TestConnectedPlugSnippetSharingSnapData(c *C) { 456 const consumerYaml = `name: consumer 457 version: 0 458 plugs: 459 content: 460 target: $SNAP_DATA/import 461 apps: 462 app: 463 command: foo 464 ` 465 consumerInfo := snaptest.MockInfo(c, consumerYaml, &snap.SideInfo{Revision: snap.R(7)}) 466 plug := interfaces.NewConnectedPlug(consumerInfo.Plugs["content"], nil, nil) 467 const producerYaml = `name: producer 468 version: 0 469 slots: 470 content: 471 write: 472 - $SNAP_DATA/export 473 ` 474 producerInfo := snaptest.MockInfo(c, producerYaml, &snap.SideInfo{Revision: snap.R(5)}) 475 slot := interfaces.NewConnectedSlot(producerInfo.Slots["content"], nil, nil) 476 477 spec := &mount.Specification{} 478 c.Assert(spec.AddConnectedPlug(s.iface, plug, slot), IsNil) 479 expectedMnt := []osutil.MountEntry{{ 480 Name: "/var/snap/producer/5/export", 481 Dir: "/var/snap/consumer/7/import", 482 Options: []string{"bind"}, 483 }} 484 c.Assert(spec.MountEntries(), DeepEquals, expectedMnt) 485 486 apparmorSpec := &apparmor.Specification{} 487 err := apparmorSpec.AddConnectedPlug(s.iface, plug, slot) 488 c.Assert(err, IsNil) 489 c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) 490 expected := ` 491 # In addition to the bind mount, add any AppArmor rules so that 492 # snaps may directly access the slot implementation's files. Due 493 # to a limitation in the kernel's LSM hooks for AF_UNIX, these 494 # are needed for using named sockets within the exported 495 # directory. 496 /var/snap/producer/5/export/** mrwklix, 497 ` 498 c.Assert(apparmorSpec.SnippetForTag("snap.consumer.app"), Equals, expected) 499 500 updateNS := apparmorSpec.UpdateNS() 501 profile0 := ` # Read-write content sharing consumer:content -> producer:content (w#0) 502 mount options=(bind, rw) /var/snap/producer/5/export/ -> /var/snap/consumer/7/import{,-[0-9]*}/, 503 mount options=(rprivate) -> /var/snap/consumer/7/import{,-[0-9]*}/, 504 umount /var/snap/consumer/7/import{,-[0-9]*}/, 505 # Writable directory /var/snap/producer/5/export 506 /var/snap/producer/5/export/ rw, 507 /var/snap/producer/5/ rw, 508 /var/snap/producer/ rw, 509 # Writable directory /var/snap/consumer/7/import 510 /var/snap/consumer/7/import/ rw, 511 /var/snap/consumer/7/ rw, 512 /var/snap/consumer/ rw, 513 # Writable directory /var/snap/consumer/7/import-[0-9]* 514 /var/snap/consumer/7/import-[0-9]*/ rw, 515 ` 516 c.Assert(strings.Join(updateNS[:], ""), Equals, profile0) 517 } 518 519 // Check that sharing of writable common data is possible 520 func (s *ContentSuite) TestConnectedPlugSnippetSharingSnapCommon(c *C) { 521 const consumerYaml = `name: consumer 522 version: 0 523 plugs: 524 content: 525 target: $SNAP_COMMON/import 526 apps: 527 app: 528 command: foo 529 ` 530 consumerInfo := snaptest.MockInfo(c, consumerYaml, &snap.SideInfo{Revision: snap.R(7)}) 531 plug := interfaces.NewConnectedPlug(consumerInfo.Plugs["content"], nil, nil) 532 const producerYaml = `name: producer 533 version: 0 534 slots: 535 content: 536 write: 537 - $SNAP_COMMON/export 538 ` 539 producerInfo := snaptest.MockInfo(c, producerYaml, &snap.SideInfo{Revision: snap.R(5)}) 540 slot := interfaces.NewConnectedSlot(producerInfo.Slots["content"], nil, nil) 541 542 spec := &mount.Specification{} 543 c.Assert(spec.AddConnectedPlug(s.iface, plug, slot), IsNil) 544 expectedMnt := []osutil.MountEntry{{ 545 Name: "/var/snap/producer/common/export", 546 Dir: "/var/snap/consumer/common/import", 547 Options: []string{"bind"}, 548 }} 549 c.Assert(spec.MountEntries(), DeepEquals, expectedMnt) 550 551 apparmorSpec := &apparmor.Specification{} 552 err := apparmorSpec.AddConnectedPlug(s.iface, plug, slot) 553 c.Assert(err, IsNil) 554 c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) 555 expected := ` 556 # In addition to the bind mount, add any AppArmor rules so that 557 # snaps may directly access the slot implementation's files. Due 558 # to a limitation in the kernel's LSM hooks for AF_UNIX, these 559 # are needed for using named sockets within the exported 560 # directory. 561 /var/snap/producer/common/export/** mrwklix, 562 ` 563 c.Assert(apparmorSpec.SnippetForTag("snap.consumer.app"), Equals, expected) 564 565 updateNS := apparmorSpec.UpdateNS() 566 profile0 := ` # Read-write content sharing consumer:content -> producer:content (w#0) 567 mount options=(bind, rw) /var/snap/producer/common/export/ -> /var/snap/consumer/common/import{,-[0-9]*}/, 568 mount options=(rprivate) -> /var/snap/consumer/common/import{,-[0-9]*}/, 569 umount /var/snap/consumer/common/import{,-[0-9]*}/, 570 # Writable directory /var/snap/producer/common/export 571 /var/snap/producer/common/export/ rw, 572 /var/snap/producer/common/ rw, 573 /var/snap/producer/ rw, 574 # Writable directory /var/snap/consumer/common/import 575 /var/snap/consumer/common/import/ rw, 576 /var/snap/consumer/common/ rw, 577 /var/snap/consumer/ rw, 578 # Writable directory /var/snap/consumer/common/import-[0-9]* 579 /var/snap/consumer/common/import-[0-9]*/ rw, 580 ` 581 c.Assert(strings.Join(updateNS[:], ""), Equals, profile0) 582 } 583 584 func (s *ContentSuite) TestInterfaces(c *C) { 585 c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface) 586 } 587 588 func (s *ContentSuite) TestModernContentInterface(c *C) { 589 plug := MockPlug(c, `name: consumer 590 version: 0 591 plugs: 592 content: 593 target: $SNAP_COMMON/import 594 apps: 595 app: 596 command: foo 597 `, &snap.SideInfo{Revision: snap.R(1)}, "content") 598 connectedPlug := interfaces.NewConnectedPlug(plug, nil, nil) 599 600 slot := MockSlot(c, `name: producer 601 version: 0 602 slots: 603 content: 604 source: 605 read: 606 - $SNAP_COMMON/read-common 607 - $SNAP_DATA/read-data 608 - $SNAP/read-snap 609 write: 610 - $SNAP_COMMON/write-common 611 - $SNAP_DATA/write-data 612 `, &snap.SideInfo{Revision: snap.R(2)}, "content") 613 connectedSlot := interfaces.NewConnectedSlot(slot, nil, nil) 614 615 // Create the mount and apparmor specifications. 616 mountSpec := &mount.Specification{} 617 c.Assert(mountSpec.AddConnectedPlug(s.iface, connectedPlug, connectedSlot), IsNil) 618 apparmorSpec := &apparmor.Specification{} 619 c.Assert(apparmorSpec.AddConnectedPlug(s.iface, connectedPlug, connectedSlot), IsNil) 620 621 // Analyze the mount specification. 622 expectedMnt := []osutil.MountEntry{{ 623 Name: "/var/snap/producer/common/read-common", 624 Dir: "/var/snap/consumer/common/import/read-common", 625 Options: []string{"bind", "ro"}, 626 }, { 627 Name: "/var/snap/producer/2/read-data", 628 Dir: "/var/snap/consumer/common/import/read-data", 629 Options: []string{"bind", "ro"}, 630 }, { 631 Name: "/snap/producer/2/read-snap", 632 Dir: "/var/snap/consumer/common/import/read-snap", 633 Options: []string{"bind", "ro"}, 634 }, { 635 Name: "/var/snap/producer/common/write-common", 636 Dir: "/var/snap/consumer/common/import/write-common", 637 Options: []string{"bind"}, 638 }, { 639 Name: "/var/snap/producer/2/write-data", 640 Dir: "/var/snap/consumer/common/import/write-data", 641 Options: []string{"bind"}, 642 }} 643 c.Assert(mountSpec.MountEntries(), DeepEquals, expectedMnt) 644 645 // Analyze the apparmor specification. 646 c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) 647 expected := ` 648 # In addition to the bind mount, add any AppArmor rules so that 649 # snaps may directly access the slot implementation's files. Due 650 # to a limitation in the kernel's LSM hooks for AF_UNIX, these 651 # are needed for using named sockets within the exported 652 # directory. 653 /var/snap/producer/common/write-common/** mrwklix, 654 /var/snap/producer/2/write-data/** mrwklix, 655 656 # In addition to the bind mount, add any AppArmor rules so that 657 # snaps may directly access the slot implementation's files 658 # read-only. 659 /var/snap/producer/common/read-common/** mrkix, 660 /var/snap/producer/2/read-data/** mrkix, 661 /snap/producer/2/read-snap/** mrkix, 662 ` 663 c.Assert(apparmorSpec.SnippetForTag("snap.consumer.app"), Equals, expected) 664 665 updateNS := apparmorSpec.UpdateNS() 666 profile0 := ` # Read-write content sharing consumer:content -> producer:content (w#0) 667 mount options=(bind, rw) /var/snap/producer/common/write-common/ -> /var/snap/consumer/common/import/write-common{,-[0-9]*}/, 668 mount options=(rprivate) -> /var/snap/consumer/common/import/write-common{,-[0-9]*}/, 669 umount /var/snap/consumer/common/import/write-common{,-[0-9]*}/, 670 # Writable directory /var/snap/producer/common/write-common 671 /var/snap/producer/common/write-common/ rw, 672 /var/snap/producer/common/ rw, 673 /var/snap/producer/ rw, 674 # Writable directory /var/snap/consumer/common/import/write-common 675 /var/snap/consumer/common/import/write-common/ rw, 676 /var/snap/consumer/common/import/ rw, 677 /var/snap/consumer/common/ rw, 678 /var/snap/consumer/ rw, 679 # Writable directory /var/snap/consumer/common/import/write-common-[0-9]* 680 /var/snap/consumer/common/import/write-common-[0-9]*/ rw, 681 ` 682 // Find the slice that describes profile0 by looking for the first unique 683 // line of the next profile. 684 start := 0 685 end, _ := apparmorSpec.UpdateNSIndexOf(" # Read-write content sharing consumer:content -> producer:content (w#1)\n") 686 c.Assert(strings.Join(updateNS[start:end], ""), Equals, profile0) 687 688 profile1 := ` # Read-write content sharing consumer:content -> producer:content (w#1) 689 mount options=(bind, rw) /var/snap/producer/2/write-data/ -> /var/snap/consumer/common/import/write-data{,-[0-9]*}/, 690 mount options=(rprivate) -> /var/snap/consumer/common/import/write-data{,-[0-9]*}/, 691 umount /var/snap/consumer/common/import/write-data{,-[0-9]*}/, 692 # Writable directory /var/snap/producer/2/write-data 693 /var/snap/producer/2/write-data/ rw, 694 /var/snap/producer/2/ rw, 695 # Writable directory /var/snap/consumer/common/import/write-data 696 /var/snap/consumer/common/import/write-data/ rw, 697 # Writable directory /var/snap/consumer/common/import/write-data-[0-9]* 698 /var/snap/consumer/common/import/write-data-[0-9]*/ rw, 699 ` 700 // Find the slice that describes profile1 by looking for the first unique 701 // line of the next profile. 702 start = end 703 end, _ = apparmorSpec.UpdateNSIndexOf(" # Read-only content sharing consumer:content -> producer:content (r#0)\n") 704 c.Assert(strings.Join(updateNS[start:end], ""), Equals, profile1) 705 706 profile2 := ` # Read-only content sharing consumer:content -> producer:content (r#0) 707 mount options=(bind) /var/snap/producer/common/read-common/ -> /var/snap/consumer/common/import/read-common{,-[0-9]*}/, 708 remount options=(bind, ro) /var/snap/consumer/common/import/read-common{,-[0-9]*}/, 709 mount options=(rprivate) -> /var/snap/consumer/common/import/read-common{,-[0-9]*}/, 710 umount /var/snap/consumer/common/import/read-common{,-[0-9]*}/, 711 # Writable directory /var/snap/producer/common/read-common 712 /var/snap/producer/common/read-common/ rw, 713 # Writable directory /var/snap/consumer/common/import/read-common 714 /var/snap/consumer/common/import/read-common/ rw, 715 # Writable directory /var/snap/consumer/common/import/read-common-[0-9]* 716 /var/snap/consumer/common/import/read-common-[0-9]*/ rw, 717 ` 718 // Find the slice that describes profile2 by looking for the first unique 719 // line of the next profile. 720 start = end 721 end, _ = apparmorSpec.UpdateNSIndexOf(" # Read-only content sharing consumer:content -> producer:content (r#1)\n") 722 c.Assert(strings.Join(updateNS[start:end], ""), Equals, profile2) 723 724 profile3 := ` # Read-only content sharing consumer:content -> producer:content (r#1) 725 mount options=(bind) /var/snap/producer/2/read-data/ -> /var/snap/consumer/common/import/read-data{,-[0-9]*}/, 726 remount options=(bind, ro) /var/snap/consumer/common/import/read-data{,-[0-9]*}/, 727 mount options=(rprivate) -> /var/snap/consumer/common/import/read-data{,-[0-9]*}/, 728 umount /var/snap/consumer/common/import/read-data{,-[0-9]*}/, 729 # Writable directory /var/snap/producer/2/read-data 730 /var/snap/producer/2/read-data/ rw, 731 # Writable directory /var/snap/consumer/common/import/read-data 732 /var/snap/consumer/common/import/read-data/ rw, 733 # Writable directory /var/snap/consumer/common/import/read-data-[0-9]* 734 /var/snap/consumer/common/import/read-data-[0-9]*/ rw, 735 ` 736 // Find the slice that describes profile3 by looking for the first unique 737 // line of the next profile. 738 start = end 739 end, _ = apparmorSpec.UpdateNSIndexOf(" # Read-only content sharing consumer:content -> producer:content (r#2)\n") 740 c.Assert(strings.Join(updateNS[start:end], ""), Equals, profile3) 741 742 profile4 := ` # Read-only content sharing consumer:content -> producer:content (r#2) 743 mount options=(bind) /snap/producer/2/read-snap/ -> /var/snap/consumer/common/import/read-snap{,-[0-9]*}/, 744 remount options=(bind, ro) /var/snap/consumer/common/import/read-snap{,-[0-9]*}/, 745 mount options=(rprivate) -> /var/snap/consumer/common/import/read-snap{,-[0-9]*}/, 746 umount /var/snap/consumer/common/import/read-snap{,-[0-9]*}/, 747 # Writable mimic /snap/producer/2 748 # .. permissions for traversing the prefix that is assumed to exist 749 # .. variant with mimic at / 750 # Allow reading the mimic directory, it must exist in the first place. 751 / r, 752 # Allow setting the read-only directory aside via a bind mount. 753 /tmp/.snap/ rw, 754 mount options=(rbind, rw) / -> /tmp/.snap/, 755 # Allow mounting tmpfs over the read-only directory. 756 mount fstype=tmpfs options=(rw) tmpfs -> /, 757 # Allow creating empty files and directories for bind mounting things 758 # to reconstruct the now-writable parent directory. 759 /tmp/.snap/*/ rw, 760 /*/ rw, 761 mount options=(rbind, rw) /tmp/.snap/*/ -> /*/, 762 /tmp/.snap/* rw, 763 /* rw, 764 mount options=(bind, rw) /tmp/.snap/* -> /*, 765 # Allow unmounting the auxiliary directory. 766 # TODO: use fstype=tmpfs here for more strictness (LP: #1613403) 767 mount options=(rprivate) -> /tmp/.snap/, 768 umount /tmp/.snap/, 769 # Allow unmounting the destination directory as well as anything 770 # inside. This lets us perform the undo plan in case the writable 771 # mimic fails. 772 mount options=(rprivate) -> /, 773 mount options=(rprivate) -> /*, 774 mount options=(rprivate) -> /*/, 775 umount /, 776 umount /*, 777 umount /*/, 778 # .. variant with mimic at /snap/ 779 /snap/ r, 780 /tmp/.snap/snap/ rw, 781 mount options=(rbind, rw) /snap/ -> /tmp/.snap/snap/, 782 mount fstype=tmpfs options=(rw) tmpfs -> /snap/, 783 /tmp/.snap/snap/*/ rw, 784 /snap/*/ rw, 785 mount options=(rbind, rw) /tmp/.snap/snap/*/ -> /snap/*/, 786 /tmp/.snap/snap/* rw, 787 /snap/* rw, 788 mount options=(bind, rw) /tmp/.snap/snap/* -> /snap/*, 789 mount options=(rprivate) -> /tmp/.snap/snap/, 790 umount /tmp/.snap/snap/, 791 mount options=(rprivate) -> /snap/, 792 mount options=(rprivate) -> /snap/*, 793 mount options=(rprivate) -> /snap/*/, 794 umount /snap/, 795 umount /snap/*, 796 umount /snap/*/, 797 # .. variant with mimic at /snap/producer/ 798 /snap/producer/ r, 799 /tmp/.snap/snap/producer/ rw, 800 mount options=(rbind, rw) /snap/producer/ -> /tmp/.snap/snap/producer/, 801 mount fstype=tmpfs options=(rw) tmpfs -> /snap/producer/, 802 /tmp/.snap/snap/producer/*/ rw, 803 /snap/producer/*/ rw, 804 mount options=(rbind, rw) /tmp/.snap/snap/producer/*/ -> /snap/producer/*/, 805 /tmp/.snap/snap/producer/* rw, 806 /snap/producer/* rw, 807 mount options=(bind, rw) /tmp/.snap/snap/producer/* -> /snap/producer/*, 808 mount options=(rprivate) -> /tmp/.snap/snap/producer/, 809 umount /tmp/.snap/snap/producer/, 810 mount options=(rprivate) -> /snap/producer/, 811 mount options=(rprivate) -> /snap/producer/*, 812 mount options=(rprivate) -> /snap/producer/*/, 813 umount /snap/producer/, 814 umount /snap/producer/*, 815 umount /snap/producer/*/, 816 # .. variant with mimic at /snap/producer/2/ 817 /snap/producer/2/ r, 818 /tmp/.snap/snap/producer/2/ rw, 819 mount options=(rbind, rw) /snap/producer/2/ -> /tmp/.snap/snap/producer/2/, 820 mount fstype=tmpfs options=(rw) tmpfs -> /snap/producer/2/, 821 /tmp/.snap/snap/producer/2/*/ rw, 822 /snap/producer/2/*/ rw, 823 mount options=(rbind, rw) /tmp/.snap/snap/producer/2/*/ -> /snap/producer/2/*/, 824 /tmp/.snap/snap/producer/2/* rw, 825 /snap/producer/2/* rw, 826 mount options=(bind, rw) /tmp/.snap/snap/producer/2/* -> /snap/producer/2/*, 827 mount options=(rprivate) -> /tmp/.snap/snap/producer/2/, 828 umount /tmp/.snap/snap/producer/2/, 829 mount options=(rprivate) -> /snap/producer/2/, 830 mount options=(rprivate) -> /snap/producer/2/*, 831 mount options=(rprivate) -> /snap/producer/2/*/, 832 umount /snap/producer/2/, 833 umount /snap/producer/2/*, 834 umount /snap/producer/2/*/, 835 # Writable directory /var/snap/consumer/common/import/read-snap 836 /var/snap/consumer/common/import/read-snap/ rw, 837 # Writable directory /var/snap/consumer/common/import/read-snap-[0-9]* 838 /var/snap/consumer/common/import/read-snap-[0-9]*/ rw, 839 ` 840 // Find the slice that describes profile4 by looking till the end of the list. 841 start = end 842 c.Assert(strings.Join(updateNS[start:], ""), Equals, profile4) 843 c.Assert(strings.Join(updateNS, ""), DeepEquals, strings.Join([]string{profile0, profile1, profile2, profile3, profile4}, "")) 844 } 845 846 func (s *ContentSuite) TestModernContentInterfacePlugins(c *C) { 847 // Define one app snap and two snaps plugin snaps. 848 plug := MockPlug(c, `name: app 849 version: 0 850 plugs: 851 plugins: 852 interface: content 853 content: plugin-for-app 854 target: $SNAP/plugins 855 apps: 856 app: 857 command: foo 858 859 `, &snap.SideInfo{Revision: snap.R(1)}, "plugins") 860 connectedPlug := interfaces.NewConnectedPlug(plug, nil, nil) 861 862 // XXX: realistically the plugin may be a single file and we don't support 863 // those very well. 864 slotOne := MockSlot(c, `name: plugin-one 865 version: 0 866 slots: 867 plugin-for-app: 868 interface: content 869 source: 870 read: [$SNAP/plugin] 871 `, &snap.SideInfo{Revision: snap.R(1)}, "plugin-for-app") 872 connectedSlotOne := interfaces.NewConnectedSlot(slotOne, nil, nil) 873 874 slotTwo := MockSlot(c, `name: plugin-two 875 version: 0 876 slots: 877 plugin-for-app: 878 interface: content 879 source: 880 read: [$SNAP/plugin] 881 `, &snap.SideInfo{Revision: snap.R(1)}, "plugin-for-app") 882 connectedSlotTwo := interfaces.NewConnectedSlot(slotTwo, nil, nil) 883 884 // Create the mount and apparmor specifications. 885 mountSpec := &mount.Specification{} 886 apparmorSpec := &apparmor.Specification{} 887 for _, connectedSlot := range []*interfaces.ConnectedSlot{connectedSlotOne, connectedSlotTwo} { 888 c.Assert(mountSpec.AddConnectedPlug(s.iface, connectedPlug, connectedSlot), IsNil) 889 c.Assert(apparmorSpec.AddConnectedPlug(s.iface, connectedPlug, connectedSlot), IsNil) 890 } 891 892 // Analyze the mount specification. 893 expectedMnt := []osutil.MountEntry{{ 894 Name: "/snap/plugin-one/1/plugin", 895 Dir: "/snap/app/1/plugins/plugin", 896 Options: []string{"bind", "ro"}, 897 }, { 898 Name: "/snap/plugin-two/1/plugin", 899 Dir: "/snap/app/1/plugins/plugin-2", 900 Options: []string{"bind", "ro"}, 901 }} 902 c.Assert(mountSpec.MountEntries(), DeepEquals, expectedMnt) 903 904 // Analyze the apparmor specification. 905 // 906 // NOTE: the paths below refer to the original locations and are *NOT* 907 // altered like the mount entries above. This is intended. See the comment 908 // below for explanation as to why those are necessary. 909 c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.app.app"}) 910 expected := ` 911 # In addition to the bind mount, add any AppArmor rules so that 912 # snaps may directly access the slot implementation's files 913 # read-only. 914 /snap/plugin-one/1/plugin/** mrkix, 915 916 917 # In addition to the bind mount, add any AppArmor rules so that 918 # snaps may directly access the slot implementation's files 919 # read-only. 920 /snap/plugin-two/1/plugin/** mrkix, 921 ` 922 c.Assert(apparmorSpec.SnippetForTag("snap.app.app"), Equals, expected) 923 } 924 925 func (s *ContentSuite) TestModernContentSameReadAndWriteClash(c *C) { 926 plug := MockPlug(c, `name: consumer 927 version: 0 928 plugs: 929 content: 930 target: $SNAP_COMMON/import 931 apps: 932 app: 933 command: foo 934 `, &snap.SideInfo{Revision: snap.R(1)}, "content") 935 connectedPlug := interfaces.NewConnectedPlug(plug, nil, nil) 936 937 slot := MockSlot(c, `name: producer 938 version: 0 939 slots: 940 content: 941 source: 942 read: 943 - $SNAP_DATA/directory 944 write: 945 - $SNAP_DATA/directory 946 `, &snap.SideInfo{Revision: snap.R(2)}, "content") 947 connectedSlot := interfaces.NewConnectedSlot(slot, nil, nil) 948 949 // Create the mount and apparmor specifications. 950 mountSpec := &mount.Specification{} 951 c.Assert(mountSpec.AddConnectedPlug(s.iface, connectedPlug, connectedSlot), IsNil) 952 apparmorSpec := &apparmor.Specification{} 953 c.Assert(apparmorSpec.AddConnectedPlug(s.iface, connectedPlug, connectedSlot), IsNil) 954 955 // Analyze the mount specification 956 expectedMnt := []osutil.MountEntry{{ 957 Name: "/var/snap/producer/2/directory", 958 Dir: "/var/snap/consumer/common/import/directory", 959 Options: []string{"bind", "ro"}, 960 }, { 961 Name: "/var/snap/producer/2/directory", 962 Dir: "/var/snap/consumer/common/import/directory-2", 963 Options: []string{"bind"}, 964 }} 965 c.Assert(mountSpec.MountEntries(), DeepEquals, expectedMnt) 966 967 // Analyze the apparmor specification. 968 // 969 // NOTE: Although there are duplicate entries with different permissions 970 // one is a superset of the other so they do not conflict. 971 c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) 972 expected := ` 973 # In addition to the bind mount, add any AppArmor rules so that 974 # snaps may directly access the slot implementation's files. Due 975 # to a limitation in the kernel's LSM hooks for AF_UNIX, these 976 # are needed for using named sockets within the exported 977 # directory. 978 /var/snap/producer/2/directory/** mrwklix, 979 980 # In addition to the bind mount, add any AppArmor rules so that 981 # snaps may directly access the slot implementation's files 982 # read-only. 983 /var/snap/producer/2/directory/** mrkix, 984 ` 985 c.Assert(apparmorSpec.SnippetForTag("snap.consumer.app"), Equals, expected) 986 } 987 988 // Check that slot can access shared directory in plug's namespace 989 func (s *ContentSuite) TestSlotCanAccessConnectedPlugSharedDirectory(c *C) { 990 const consumerYaml = `name: consumer 991 version: 0 992 plugs: 993 content: 994 target: $SNAP_COMMON/import 995 ` 996 consumerInfo := snaptest.MockInfo(c, consumerYaml, &snap.SideInfo{Revision: snap.R(7)}) 997 plug := interfaces.NewConnectedPlug(consumerInfo.Plugs["content"], nil, nil) 998 const producerYaml = `name: producer 999 version: 0 1000 slots: 1001 content: 1002 write: 1003 - $SNAP_COMMON/export 1004 apps: 1005 app: 1006 command: bar 1007 ` 1008 producerInfo := snaptest.MockInfo(c, producerYaml, &snap.SideInfo{Revision: snap.R(5)}) 1009 slot := interfaces.NewConnectedSlot(producerInfo.Slots["content"], nil, nil) 1010 1011 apparmorSpec := &apparmor.Specification{} 1012 err := apparmorSpec.AddConnectedSlot(s.iface, plug, slot) 1013 c.Assert(err, IsNil) 1014 c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.producer.app"}) 1015 expected := ` 1016 # When the content interface is writable, allow this slot 1017 # implementation to access the slot's exported files at the plugging 1018 # snap's mountpoint to accommodate software where the plugging app 1019 # tells the slotting app about files to share. 1020 /var/snap/consumer/common/import/** mrwklix, 1021 ` 1022 c.Assert(apparmorSpec.SnippetForTag("snap.producer.app"), Equals, expected) 1023 }