Interface request: "cups-control" on CUPS snap and including D-Bus

The interface is defined in snapd source code here: https://github.com/snapcore/snapd/blob/master/interfaces/builtin/cups_control.go

I have met @ijohnson in person and talked about the issue. Similar to Docker and pulseaudio we should have a “cups-control” interface where the CUPS snap plugs to, to provide its services consisting of a domain socket for communicating with CUPS via IPP and D-Bus services to provide notifications. Applications which want to print should plug to the “cups” interface to receive access to CUPS’ domain socket and to CUPS’ D-Bus services.
There are following D-Bus services according to “audit” messages in syslog:

  • bus=“system” path="/org/cups/cupsd/Notifier" in
    terface=“org.cups.cupsd.Notifier”
  • bus=“system” path="/com/redhat/PrinterSpooler" interface=“com.redhat.PrinterSpooler”

here are 2 “audit” messages as example:

Mar  3 09:02:38 till-x1yoga kernel: [2843974.221894] audit: type=1107 audit(1583222558.216:901456): pid=1438 uid=104 auid=4294967295 ses=4294967295 msg='apparmor="DENIED" operation="dbus_signal"  bus="system" path="/com/redhat/PrinterSpooler" interface="com.redhat.PrinterSpooler" member="PrinterRemoved" mask="send" name="org.freedesktop.DBus" pid=676810 label="snap.printing-stack-snap.cupsd" peer_pid=4920 peer_label="unconfined"
Mar  3 09:02:38 till-x1yoga kernel: [2843974.221894]  exe="/usr/bin/dbus-daemon" sauid=104 hostname=? addr=? terminal=?'
Mar  3 09:02:38 till-x1yoga kernel: [2843974.224268] audit: type=1107 audit(1583222558.216:901457): pid=1438 uid=104 auid=4294967295 ses=4294967295 msg='apparmor="DENIED" operation="dbus_method_call"  bus="system" path="/org/freedesktop/ColorManager" interface="org.freedesktop.ColorManager" member="FindDeviceById" mask="send" name="org.freedesktop.ColorManager" pid=676810 label="snap.printing-stack-snap.cupsd" peer_pid=1924921 peer_label="unconfined"

.deb-based CUPS and snapped CUPS can run in parallel, both can be accessed by domain sockets, but by different ones, .deb-based CUPS uses /var/run/cups/cups.sock, snapped CUPS uses /var/snap/printing-stack-snap/current/var/run/cups.sock for now (subject to change, as snap will get renamed to “ipp-service-cups” and as the socket is accessed externally it will probably move to /var/snap/ipp-service-cups/common/.... The D-Bus service names are the same for both. I do not know how this gets handles. The .deb-based CUPS is always on port 631, the snap goes to port 631 by default but uses 10631 when 631 is already occupied (usually by an already present .deb
-based CUPS. Both have their cups-browsed to accompany them. If both are running, they exchange shared printers like usual local and remote CUPS servers and clients. So running both could be helpful for development.
In production one should run only one, preferably the snap (which will run on port 631 then).
It should be esily possible for an application user to print, especially on the snap.
Please correct me if I wrote up something wrong here.

@jdstrand, could you have a look into this concept? Thanks.

The CUPS socket allows configuring printers in addition to printing, which is why we named it cups-control rather than cups. The rationale for not auto-connecting is described here: Process for aliases, auto-connections and tracks (see "auto-connection request considerations)).

I took a look. Rather than giving my preliminary thoughts, someone can summarize our meeting notes here.

The summary of our meeting is as follows:

  1. we want to be able to support 3 different user experiences:

    1. snap consumers already plugging cups-control to continue to be able to print to cups via distro packaging
    2. snap consumers already plugging cups-control to be able to print to cups packaged as a snap
    3. non-snap consumers to be able to print to cups packaged as a snap

    Due to the above, the snap providing cups will bind to /run/cups.sock (where /var/run is a symlink to /run) and listen on the standard 631 network port. The snap providing cups may at its discretion implement a fallback mechanism for testing purposes that puts its unix socket in SNAP_DATA/SNAP_COMMON (optionally exporting via the content interface) and listen on 10631 (or wherever)

  2. the cups-control interface will continue to support implicitClassic for 1.1, above

  3. the cups-control interface will be modified to also support ‘slot-snap-type: app’ to support 1.2, above

  4. rather than implementing an additional interface for cupsd accesses, the snap providing cups will ‘slots: cups-control’ which will grant via PermanentSlot any cupsd-specific accesses. The snap providing cups will plugs existing interfaces like ‘network’, ‘network-bind’, ‘avahi-control’, etc as needed

  5. per @till.kamppeter, the DBus interface is only about monitoring and subscribing to notifications, not about administration and these will not change. For now (though this is subject to change in PR review), we will add the appropriate DBus accesses to cups-control (esp for cupsd to bind to the interfaces and respond to requests from unconfined processes; adding the accesses for plugging snaps to use is likely also fine since they can get this information from the admin portions of the socket APIs). This helps support 1.3, above

In addition to the above, we discussed what it would take to allow auto-connection for printing. Note that printing is only done through the socket (not the DBus APIs). Also, recall that cupsd’s decision on whether to allow admin functionality to the connecting user is based on group membership. With typical desktop applications on single-user installs, the uid of the process the snap is running as will already be in this group, which means that connected snaps will typically have the necessary group membership to configure printing on the system rather than just print (which is why it is named ‘cups-control’ instead of ‘printing’ or similar). To address this, we discussed that a patch could be developed similar to what was done for pulseaudio, where cupsd would:

  • look at the PEERCRED of the connecting client process to obtain the pid/etc of the process
  • in addition to cupsd’s normal group checks, it would ask snapd (eg, via snapd-glib or similar) to see if the connecting process is from a snap
  • make a decision based on this information. An initial implementation might simply deny access to admin functionality if the connecting process is a snap. A future implementation might have cupsd then ask if a specific interface is connected, and if so, allow the admin access (a suggestion would be to introduce a new ‘cups’ interface that allows access to the socket and only if cups-control is connected would admin access be allowed).

This patch should be upstreamed to Apple. @till.kamppeter said he would discuss patching cupsd with @jamesh and see if a PoC could be developed in time for an upcoming OpenPrinting summit. If they are amenable to the change, we can consider changes to snapd. In the meantime, distros could choose to distro-patch as desired.

@till.kamppeter - as a future improvement to the snap, I suggested perhaps looking into using the ‘snap_daemon’ user instead of running as root. See System usernames for details and caveats.

1 Like

While not for this cycle, I’ve added these changes to my TODO, hopefully for early next cycle.

@pedronis - in addition to the above, please note that as an aside and not tied to the cups-control changes, @till.kamppeter mentioned that he is not able to create a group that the snap can consistently use to check for group membership of connecting clients for cupsd’s decision wrt access to admin APIs over the unix socket. This is similar to lxd, docker and multipass (and the related multipass request). This request is slightly different since those use 660 perms with group and cupsd uses 666 and looks at the PEERCRED.

Note that there isn’t such an API in snapd at present. The PulseAudio module I put together does this by checking the AppArmor label of the process. Xdg-desktop-portal does the same, but will be switching to check cgroups plus a call out to snap routine portal-info (it was done this way to avoid putting too much complicated logic on the x-d-p side).

Either of these strategies are possible under strict confinement with an appropriate interface, but they are not exactly simple.

I wonder if a better approach (and one more likely to be accepted upstream) would be to implement polkit support into CUPS? This has a number of benefits:

  1. It will be useful on Linux systems not running snapd, so could gain support from other distros if Apple pushes back.

  2. Polkitd can make access decisions based on the user’s host system user names or group memberships that a confined cupsd can’t see. This would make it possible to implement passwordless access for lpadmin members if desired.

  3. If configured to require the user’s password (i.e. auth_admin or auth_admin_keep), then perhaps we don’t need the snap check: the polkit dialog will allow the user to cancel administrative actions before they are completed.

This would obviously require some further improvements to snapd: namely allowing snapped daemons to talk to polkitd:

I think it is also worth keeping in mind that we have a solution available to allow applications to print without connecting the cups-control interface, in the form of xdg-desktop-portal’s print portal. Applications using GTK 3’s printing APIs can automatically use this interface. I think it would be better to steer applications towards that interface rather than following the audio-playback/audio-record model we used for Pulse Audio where the daemon makes the policy decision.

There is also cups-pk-helper which could be added to the CUPS Snap.
I never have looked deeply into it and how it works but perhaps it is helpful.

cups-pk-helper does show that there is some demand for a polkit mediation. The main problem is that to use cups-pk-helper, the client application needs to be rewritten to use its D-Bus API rather than sending IPP requests over /run/cups/cups.sock.

What I am suggesting is have cupsd make the polkit calls itself, which would allow standard cups clients to take advantage. We managed to integrate polkit support into snapd’s “HTTP over unix domain sockets” API, so it should be possible to do the same with cupsd.

Yes, the polkit-in-cupsd is principally a good idea and could be useful also for other things than only snapping CUPS, what raises the chances that Apple accepts the patch. Also for Apple to accept the patch we should use conditionals to optionally build without PolicyKit/polkit and also a directive in cups-files.conf to turn on/off PolicyKit/polkit support.
One problem is that there is PolicyKit and polkit, where the latter s the newer AFAIK. Now the question is whether we need to support both or only one of them and if only one which one.
Now if one uses PolicyKit/polkit, does this mean that I as creator of the CUPS Snap can say which group of the host system is allowed to do admin? Or can we also have two interfaces with that, one print-only and one print-and-admin?
Can we perhaps move code of the cups-pk-helper mechanism into CUPS itself to integrate the functionality in cupsd and eliminate the need of cups-pk-helper for tools like GNOME Control Center or system-config-printer?

@jamesh, @jdstrand, WDYT is the better method? The one of pulseaudio or using PolicyKit/polkit?

PolicyKit and polkit are the same thing: not two competing projects.

For the cupsd side, it would need to be able to issue an org.freedesktop.PolkicyKit1.Authority.CheckAuthorization D-Bus method call to decide whether to process a particular HTTP request. From the sound of it there is already some level of D-Bus integration for the Avahi support, so hopefully this won’t be too invasive.

As far as configuring default access, there are two parts to configuring Polkit:

  1. .policy files provided by applications. These are XML files describing the various actions the application will use, associating human readable descriptions for display in dialogs and default policy (e.g. let anyone perform the action, only admin users, only admin users after they provide their password, etc). These are not intended to be edited by users or system administrators.

  2. policy configuration files: either key file .pkla files with Ubuntu’s version of polkit, or Javascript .rules files for newer releases. These are usually provided by the distribtuion or local system administrator to augment or override the defaults from the .policy file. They are more expressive, so could be used to e.g. allow an action to be called by lpadmin group members.

At present there is no support in snapd for daemons that delegate policy decisions to polkit. In the forum thread I linked to, I was suggesting we let a snap install .policy files (after validation). I’d be wary about installing pkla/rules files, since they are not usually the domain of applications. Also, the JS versions are pretty much impossible to validate.

If Ubuntu is going to ship cups as a snap though, it could easily ship policy configuration that affects the cups snap’s actions though.

Important is that the CUPS Snap can be installed on any Snap-supporting operating system, not only Ubuntu. So we must be careful with allowing the Snap to install files out of the sandbox (*.policy) or require certain files outside the sandbox (*.rules/*.pkla) this can restrict the Snap to Ubuntu and it also can be a similar situation as the (now deprecated) printer drivers being PPD files and filters which need to be in certain file system locations, a situation we want to avoid with the new printing and scanning architecture.
Perhaps we need to go the PulseAudio way.

I’ve often said (outside of this thread) that polkit integration would be a welcome addition to cupsd. It can then listen on its socket like normal and apply polkit policies to its APIs. Presumably, all things related to printing would be allowed and all things related to admin would require auth_admin/auth_admin_keep. This would be a great addition to cupsd irrespective of snapd.

With respect to snapd:

  1. it would be easy enough to allow the cupsd providing (slot) to talk to polkit via its PermanentSlotAppArmor policy
  2. there is no polkit daemon on Ubuntu Core devices
  3. the devil is in the details wrt the polkit policy. Today, access to admin functionality is limited to group membership. I strongly suspect that for a similar UX, distros (including Ubuntu) would recreate this under polkit and say “admin APIs are allowed if user is in the lpadmin group” to avoid bugs from users complaining about needing a password to configure some aspect of a printer when they never used to. Since single user desktop installs will default to this, we are back to where we started with snap applications running in the desktop environment are usually able to access admin APIs via the socket
  4. the polkit policy will likely need to allow non-console root processes without a password so snap daemons that plugs cups-control would have access to the admin APIs
  5. snapd doesn’t currently have a polkit backend for the snap to ship policy and put in place for the host’s polkit to use
  6. the patch to cupsd would be rather extensive in comparison to the ‘pulseaudio approach’ (even considering what @jamesh reminded us about that there is no API for querying snapd so cupsd would have to implement that)

Due to ‘2’ on Ubuntu Core devices and ‘4’ in general, cupsd would need the ‘pulseaudio approach’ anyway to mediate admin access for snaps

‘3’ is a real problem when accessing cupsd coming from a deb when that deb implements polkit policy that isn’t what we would want for snaps. The “pulseaudio approach” is likely needed for this as well. While snap-confine is in a position to drop “lpadmin” from the list of supplementary groups as part of startup, this is unlikely to scale out cross-distro since it is possible that different distros will use a different group for cupsd.

‘5’ requires not insignificant work to snapd (it would be generally useful though), but is inhibited by ‘2’ (we would want to consider retroactively adding polkit to UC16, UC18 and UC20 for this). As @till.kamppeter mentioned, that implementation would need to be careful to not step on existing host policy (though, in theory we could utilize the vendor or site policy mechanisms to override deb/rpm/ubuntu/distro policy…).

(It is also theoretically possible for the printing stack snap to ship polkitd, listening on something printing stack-specific in terms of DBus and have the snap have internal polkit policy that this snap-specific-polkitd would use and cupsd would query that. This still wouldn’t address ‘4’ and for systems with only the printing stack snap installed, UX regresses due to ‘3’. Not to mention, this is pretty terrible implementation-wise. :slight_smile: )

In short, I would welcome polkit integration for cupsd, but it is unlikely to be sufficient to address all mediation points for snaps accessing the admin APIs. We’ll still need a “pulseaudio approach” IME, but just like @till.kamppeter said for polkit, this would be an optional build feature.

@jdstrand, thanks, I think that the PulseAudio way is the better one then.

@jamesh, could you do the PoC you suggsted for patching CUPS with the PulseAudio-like solution, like suggested in @jdstrand’s meeting summary and your answer? Thanks.

I’m not at all convinced that this is true. Either implementation is going to be doing roughly the same thing:

  1. do an SO_PEERCRED lookup on the socket to determine the client uid and pid.
  2. gather more information about that client from /proc/$pid
  3. for each request the client makes on the connection, check to see if it is privileged.
  4. for privileged operations, issue a request to a second daemon to check the client’s authorisation.
  5. when the second daemon’s response comes back, either serve the client’s request or return an error.

I’ve got a pretty full plate at the moment, and you’ve got a lot more experience with the CUPS source code than me. I’m happy to give you some pointers though.

That part of the code, sure, but the problem is with polkit, the APIs need to be carved up into polkit chunks that may or may not align with the current group checks. Even if they are, there is generating the xml and deciding on how the default policy should be written. This could be shortened to reimplement the group membership, but then the patch didn’t help our snap mediation needs.

@jamesh, @jdstrand, I have started the CUPS patch now (see below, is there really no way to add attachments in this forum?). I have found out that all authentication is done at a central place in cupsd, in the cupsdIsAuthorized() function in the file scheduler/auth.c.
To define which of the many IPP operations which CUPS supports are administrative operations (the ones which should only be allowed through the “cups-control” interface and not through the “cups” interface) I have taken the ones which are only allowed by the pseudo-group @SYSTEM in the policies in /etc/cups/cupsd.conf. This way I can simply check in the cupsdIsAuthorized() function whether the authorization is done through @SYSTEM and if so, call an extra function, which I have called cupsdCheckAdminTask() and only if this one tests positive, allow the operation.
In cupsdCheckAdminTask() I check whether the client connects via domain socket (network address family AF_LOCAL) and if not, the function simply passes. If the client connects through a domain socket I poll its peer credentials and so get the PID of the client process. After the line

/* Examine client process here */

one only needs to insert the checking for whether this process is from a Snap and whether this Snap plugs with “cups-control”.

Till

diff --git a/scheduler/auth.c b/scheduler/auth.c
index 4fbad6e24..466a0e529 100644
--- a/scheduler/auth.c
+++ b/scheduler/auth.c
@@ -43,6 +43,7 @@
 #  include <sys/ucred.h>
 typedef struct xucred cupsd_ucred_t;
 #  define CUPSD_UCRED_UID(c) (c).cr_uid
+#  define CUPSD_UCRED_PID(c) (c).cr_pid
 #else
 #  ifndef __OpenBSD__
 typedef struct ucred cupsd_ucred_t;
@@ -50,6 +51,7 @@ typedef struct ucred cupsd_ucred_t;
 typedef struct sockpeercred cupsd_ucred_t;
 #  endif
 #  define CUPSD_UCRED_UID(c) (c).uid
+#  define CUPSD_UCRED_PID(c) (c).pid
 #endif /* HAVE_SYS_UCRED_H */
 
 
@@ -1520,6 +1522,61 @@ cupsdFreeLocation(cupsd_location_t *loc)/* I - Location to free */
 }
 
 
+/*
+ * 'cupsdCheckAdminTask()' - Do additional checks on administrative tasks
+ */
+
+int                                      /* O - 1 if admin task authorized */
+cupsdCheckAdminTask(cupsd_client_t *con) /* I - Connection */
+{
+  cupsdLogMessage(CUPSD_LOG_DEBUG,
+		  "cupsdCheckAdminTask: ADMINISTRATIVE TASK!!");
+
+#if defined(SO_PEERCRED) && defined(AF_LOCAL)
+ /*
+  * Get the client's PID if it accesses locally via domain socket
+  */
+
+  if (httpAddrFamily(con->http->hostaddr) == AF_LOCAL)
+  {
+    cupsd_ucred_t	peercred;	/* Peer credentials */
+    socklen_t		peersize;	/* Size of peer credentials */
+    int                 client_pid;     /* PID of client */
+
+    peersize = sizeof(peercred);
+
+#  ifdef __APPLE__
+    if (getsockopt(httpGetFd(con->http), 0, LOCAL_PEERCRED, &peercred,
+		   &peersize))
+#  else
+    if (getsockopt(httpGetFd(con->http), SOL_SOCKET, SO_PEERCRED, &peercred,
+		   &peersize))
+#  endif /* __APPLE__ */
+    {
+      cupsdLogMessage(CUPSD_LOG_ERROR,
+		      "cupsdCheckAdminTask: Unable to get peer credentials - %s",
+		      strerror(errno));
+    }
+    else
+    {
+      cupsdLogMessage(CUPSD_LOG_DEBUG,
+		      "cupsdCheckAdminTask: Client UID %d PID %d",
+		      CUPSD_UCRED_UID(peercred),
+		      CUPSD_UCRED_PID(peercred));
+      client_pid = CUPSD_UCRED_PID(peercred);
+
+      /* Examine client process here */
+      cupsdLogMessage(CUPSD_LOG_DEBUG,
+		      "cupsdCheckAdminTask: Examining process %d ...",
+		      client_pid);
+    }
+  }
+#endif /* SO_PEERCRED && AF_LOCAL */
+
+  return 1;
+}
+
+
 /*
  * 'cupsdIsAuthorized()' - Check to see if the user is authorized...
  */
@@ -1714,12 +1771,9 @@ cupsdIsAuthorized(cupsd_client_t *con,	/* I - Connection */
 
  /*
   * OK, got a username.  See if we need normal user access, or group
-  * access... (root always matches)
+  * access...
   */
 
-  if (!strcmp(username, "root"))
-    return (HTTP_OK);
-
  /*
   * Strip any @domain or @KDC from the username and owner...
   */
@@ -1749,6 +1803,21 @@ cupsdIsAuthorized(cupsd_client_t *con,	/* I - Connection */
   else
     pw = NULL;
 
+ /*
+  * For matching user and group memberships below we will first go
+  * through all names except @SYSTEM to authorize the task as
+  * non-administrative, like printing or deleting one's own job, if this
+  * fails we will check whether we can authorize via the special name
+  * @SYSTEM, as an administrative task, like creating a print queue or
+  * deleting someone else's job.
+  * Note that tasks are considered as administrative by the policies
+  * in cupsd.conf, when they require the user or group @SYSTEM.
+  * We do this separation because if the client is a Snap connecting via
+  * domain socket, we need to additionally check whether it plugs to us
+  * through the "cups-control" interface which allows administration and
+  * not through the "cups" interface which allows only printing.
+  */
+
   if (best->level == CUPSD_AUTH_USER)
   {
    /*
@@ -1779,8 +1848,15 @@ cupsdIsAuthorized(cupsd_client_t *con,	/* I - Connection */
       {
 	if (!_cups_strncasecmp(name, "@AUTHKEY(", 9) && check_authref(con, name + 9))
 	  return (HTTP_OK);
-	else if (!_cups_strcasecmp(name, "@SYSTEM") && SystemGroupAuthKey &&
-		 check_authref(con, SystemGroupAuthKey))
+      }
+
+      for (name = (char *)cupsArrayFirst(best->names);
+           name;
+	   name = (char *)cupsArrayNext(best->names))
+      {
+	if (!_cups_strcasecmp(name, "@SYSTEM") && SystemGroupAuthKey &&
+	    check_authref(con, SystemGroupAuthKey) &&
+	    cupsdCheckAdminTask(con))
 	  return (HTTP_OK);
       }
 
@@ -1797,9 +1873,8 @@ cupsdIsAuthorized(cupsd_client_t *con,	/* I - Connection */
 	return (HTTP_OK);
       else if (!_cups_strcasecmp(name, "@SYSTEM"))
       {
-        for (i = 0; i < NumSystemGroups; i ++)
-	  if (cupsdCheckGroup(username, pw, SystemGroups[i]))
-	    return (HTTP_OK);
+	/* Do @SYSTEM later, when every other entry fails */
+	continue;
       }
       else if (name[0] == '@')
       {
@@ -1810,6 +1885,19 @@ cupsdIsAuthorized(cupsd_client_t *con,	/* I - Connection */
         return (HTTP_OK);
     }
 
+    for (name = (char *)cupsArrayFirst(best->names);
+	 name;
+	 name = (char *)cupsArrayNext(best->names))
+    {
+      if (!_cups_strcasecmp(name, "@SYSTEM"))
+      {
+        for (i = 0; i < NumSystemGroups; i ++)
+	  if (cupsdCheckGroup(username, pw, SystemGroups[i]) &&
+	      cupsdCheckAdminTask(con))
+	    return (HTTP_OK);
+      }
+    }
+
     return (con->username[0] ? HTTP_FORBIDDEN : HTTP_UNAUTHORIZED);
   }
 
@@ -1827,16 +1915,31 @@ cupsdIsAuthorized(cupsd_client_t *con,	/* I - Connection */
        name;
        name = (char *)cupsArrayNext(best->names))
   {
+    if (!_cups_strcasecmp(name, "@SYSTEM"))
+    {
+      /* Do @SYSTEM later, when every other entry fails */
+      continue;
+    }
+
     cupsdLogMessage(CUPSD_LOG_DEBUG2, "cupsdIsAuthorized: Checking group \"%s\" membership...", name);
 
+    if (cupsdCheckGroup(username, pw, name))
+      return (HTTP_OK);
+  }
+
+  for (name = (char *)cupsArrayFirst(best->names);
+       name;
+       name = (char *)cupsArrayNext(best->names))
+  {
     if (!_cups_strcasecmp(name, "@SYSTEM"))
     {
+      cupsdLogMessage(CUPSD_LOG_DEBUG2, "cupsdIsAuthorized: Checking group \"%s\" membership...", name);
+
       for (i = 0; i < NumSystemGroups; i ++)
-	if (cupsdCheckGroup(username, pw, SystemGroups[i]))
+	if (cupsdCheckGroup(username, pw, SystemGroups[i]) &&
+	    cupsdCheckAdminTask(con))
 	  return (HTTP_OK);
     }
-    else if (cupsdCheckGroup(username, pw, name))
-      return (HTTP_OK);
   }
 
  /*