File: cups.go

package info (click to toggle)
snapd 2.71-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 79,536 kB
  • sloc: ansic: 16,114; sh: 16,105; python: 9,941; makefile: 1,890; exp: 190; awk: 40; xml: 22
file content (218 lines) | stat: -rw-r--r-- 8,468 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
 * Copyright (C) 2020 Canonical Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package builtin

import (
	"fmt"
	"strings"

	"github.com/snapcore/snapd/interfaces"
	"github.com/snapcore/snapd/interfaces/apparmor"
	"github.com/snapcore/snapd/interfaces/mount"
	"github.com/snapcore/snapd/osutil"
	"github.com/snapcore/snapd/snap"
)

// On systems where the slot is provided by an app snap, the cups interface is
// the companion interface to the cups-control interface. The design of these
// interfaces is based on the idea that the slot implementation (eg cupsd) is
// expected to query snapd to determine if the cups-control interface is
// connected or not for the peer client process and the print service will
// mediate admin functionality (ie, the rules in these interfaces allow
// connecting to the print service, but do not implement enforcement rules; it
// is up to the print service to provide enforcement).
const cupsSummary = `allows access to the CUPS socket for printing`

// cups is currently only available via a providing app snap and this interface
// assumes that the providing app snap also slots 'cups-control' (the current
// design allows the snap provider to slots both cups-control and cups or just
// cups-control (like with implicit classic or any slot provider without
// mediation patches), but not just cups).
const cupsBaseDeclarationSlots = `
  cups:
    allow-installation:
      slot-snap-type:
        - app
    deny-connection: true
    deny-auto-connection: true
`

const cupsConnectedPlugAppArmor = `
# Allow communicating with the cups server

# Do not allow reading the user or global client.conf for this snap, as this may
# allow a user to point an application at an unconfined cupsd which could be 
# used to load printer drivers etc. We only want client snaps with the cups 
# interface plug connected to be able to talk to a version of cupsd which is
# strictly confined and performs mediation. This means only allowing to talk to
# /var/cups/cups.sock and not /run/cups/cups.sock since snapd has no way to know
# if the latter cupsd is confined and performs mediation, but the upstream 
# maintained cups snap providing a cups slot will always perform mediation.
# As such, do not use the <abstractions/cups-client> include file here.

# Allow reading the personal settings for cups like default printer, etc.
owner @{HOME}/.cups/lpoptions r,

/{,var/}run/cups/printcap r,

# Allow talking to the snap version of cupsd socket that we expose via bind 
# mounts from a snap providing the cups slot to this snap.
/var/cups/cups.sock rw,
`

type cupsInterface struct {
	commonInterface
}

func (iface *cupsInterface) AppArmorConnectedSlot(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
	return nil
}

func validateCupsSocketDirSlotAttr(a interfaces.Attrer, snapInfo *snap.Info) (string, error) {
	// Allow an empty specification for the slot, in which case we don't perform
	// any mounts, etc. This is mainly to prevent errors in systems which still
	// have the old cups snap installed that haven't been updated to use the new
	// snap with the new slot declaration
	if _, ok := a.Lookup("cups-socket-directory"); !ok {
		return "", nil
	}

	var cupsdSocketSourceDir string
	if err := a.Attr("cups-socket-directory", &cupsdSocketSourceDir); err != nil {
		return "", err
	}

	// make sure that the cups socket dir is not an AppArmor Regular expression
	if err := apparmor.ValidateNoAppArmorRegexp(cupsdSocketSourceDir); err != nil {
		return "", fmt.Errorf("cups-socket-directory is not usable: %v", err)
	}

	if !cleanSubPath(cupsdSocketSourceDir) {
		return "", fmt.Errorf("cups-socket-directory is not clean: %q", cupsdSocketSourceDir)
	}

	// validate that the setting for cups-socket-directory is in $SNAP_DATA or
	// $SNAP_COMMON, we don't allow any other directories for the slot socket
	// dir
	// TODO: should we also allow /run/$SNAP_INSTANCE_NAME/ too ?
	if !strings.HasPrefix(cupsdSocketSourceDir, "$SNAP_COMMON") && !strings.HasPrefix(cupsdSocketSourceDir, "$SNAP_DATA") {
		return "", fmt.Errorf("cups-socket-directory must be a directory of $SNAP_COMMON or $SNAP_DATA")
	}
	// otherwise it must have a prefix of either SNAP_COMMON or SNAP_DATA,
	// validate that it has no other variables in it
	err := snap.ValidatePathVariables(cupsdSocketSourceDir)
	if err != nil {
		return "", err
	}

	// The path starts with $ and ValidatePathVariables() ensures
	// path contains only $SNAP, $SNAP_DATA, $SNAP_COMMON, and no
	// other $VARs are present. It is ok to use
	// ExpandSnapVariables() since it only expands $SNAP, $SNAP_DATA
	// and $SNAP_COMMON
	return snapInfo.ExpandSnapVariables(cupsdSocketSourceDir), nil
}

func (iface *cupsInterface) BeforePrepareSlot(slot *snap.SlotInfo) error {
	// verify that the snap has a cups-socket-directory interface attribute, which is
	// needed to identify where to find the cups socket is located in the snap
	// providing the cups socket
	_, err := validateCupsSocketDirSlotAttr(slot, slot.Snap)
	return err
}

func (iface *cupsInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
	cupsdSocketSourceDir, err := validateCupsSocketDirSlotAttr(slot, slot.Snap())
	if err != nil {
		return err
	}

	// add the base snippet
	spec.AddSnippet(cupsConnectedPlugAppArmor)

	if cupsdSocketSourceDir == "" {
		// no other rules, this is the legacy slot without the additional
		// attribute
		return nil
	}

	// add rules to access the socket dir from the slot location directly
	// this is necessary otherwise clients get denials like this:
	// apparmor="DENIED" operation="connect"
	// profile="snap.test-snapd-cups-consumer.bin"
	// name="/var/snap/test-snapd-cups-provider/common/cups.sock"
	// pid=3195747 comm="nc" requested_mask="wr" denied_mask="wr" fsuid=0 ouid=0
	// this denial is the same that would happen for the content interface, so
	// we employ the same workaround from the content interface here too
	spec.AddSnippet(fmt.Sprintf(`
# In addition to the bind mount, add any AppArmor rules so that
# snaps may directly access the slot implementation's files. Due
# to a limitation in the kernel's LSM hooks for AF_UNIX, these
# are needed for using named sockets within the exported
# directory.
"%s/**" mrwklix,`, cupsdSocketSourceDir))

	// setup the snap-update-ns rules for bind mounting for the plugging snap
	emit := spec.AddUpdateNSf

	emit("  # Mount cupsd socket from cups snap to client snap\n")
	// note the trailing "/" is needed - we ensured that cupsdSocketSourceDir is
	// clean when we validated it, so it will not have a trailing "/" so we are
	// safe to add this here
	emit("  mount options=(rw bind) \"%s/\" -> /var/cups/,\n", cupsdSocketSourceDir)
	emit("  umount /var/cups/,\n")

	apparmor.GenWritableProfile(emit, cupsdSocketSourceDir, 1)
	apparmor.GenWritableProfile(emit, "/var/cups", 1)

	return nil
}

func (iface *cupsInterface) MountConnectedPlug(spec *mount.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
	cupsdSocketSourceDir, err := validateCupsSocketDirSlotAttr(slot, slot.Snap())
	if err != nil {
		return err
	}

	if cupsdSocketSourceDir == "" {
		// no other rules, this is the legacy slot without the additional
		// attribute
		return nil
	}

	// add a bind mount of the cups-socket-directory to /var/cups of the plugging snap
	return spec.AddMountEntry(osutil.MountEntry{
		Name:    cupsdSocketSourceDir,
		Dir:     "/var/cups/",
		Options: []string{"bind", "rw"},
	})
}

func init() {
	registerIface(&cupsInterface{
		commonInterface: commonInterface{
			name:                 "cups",
			summary:              cupsSummary,
			implicitOnCore:       false,
			implicitOnClassic:    false,
			baseDeclarationSlots: cupsBaseDeclarationSlots,
		},
	})
}