1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 inherit (pkgs) cups-pk-helper cups-filters xdg-utils;
8
9 cfg = config.services.printing;
10 cups = cfg.package;
11
12 avahiEnabled = config.services.avahi.enable;
13 polkitEnabled = config.security.polkit.enable;
14
15 additionalBackends = pkgs.runCommand "additional-cups-backends" {
16 preferLocalBuild = true;
17 } ''
18 mkdir -p $out
19 if [ ! -e ${cups.out}/lib/cups/backend/smb ]; then
20 mkdir -p $out/lib/cups/backend
21 ln -sv ${pkgs.samba}/bin/smbspool $out/lib/cups/backend/smb
22 fi
23
24 # Provide support for printing via HTTPS.
25 if [ ! -e ${cups.out}/lib/cups/backend/https ]; then
26 mkdir -p $out/lib/cups/backend
27 ln -sv ${cups.out}/lib/cups/backend/ipp $out/lib/cups/backend/https
28 fi
29 '';
30
31 # Here we can enable additional backends, filters, etc. that are not
32 # part of CUPS itself, e.g. the SMB backend is part of Samba. Since
33 # we can't update ${cups.out}/lib/cups itself, we create a symlink tree
34 # here and add the additional programs. The ServerBin directive in
35 # cups-files.conf tells cupsd to use this tree.
36 bindir = pkgs.buildEnv {
37 name = "cups-progs";
38 paths =
39 [ cups.out additionalBackends cups-filters pkgs.ghostscript ]
40 ++ cfg.drivers;
41 pathsToLink = [ "/lib" "/share/cups" "/bin" ];
42 postBuild = cfg.bindirCmds;
43 ignoreCollisions = true;
44 };
45
46 writeConf = name: text: pkgs.writeTextFile {
47 inherit name text;
48 destination = "/etc/cups/${name}";
49 };
50
51 cupsFilesFile = writeConf "cups-files.conf" ''
52 SystemGroup root wheel
53
54 ServerBin ${bindir}/lib/cups
55 DataDir ${bindir}/share/cups
56 DocumentRoot ${cups.out}/share/doc/cups
57
58 AccessLog syslog
59 ErrorLog syslog
60 PageLog syslog
61
62 TempDir ${cfg.tempDir}
63
64 SetEnv PATH /var/lib/cups/path/lib/cups/filter:/var/lib/cups/path/bin
65
66 # User and group used to run external programs, including
67 # those that actually send the job to the printer. Note that
68 # Udev sets the group of printer devices to `lp', so we want
69 # these programs to run as `lp' as well.
70 User cups
71 Group lp
72
73 ${cfg.extraFilesConf}
74 '';
75
76 cupsdFile = writeConf "cupsd.conf" ''
77 ${concatMapStrings (addr: ''
78 Listen ${addr}
79 '') cfg.listenAddresses}
80 Listen /run/cups/cups.sock
81
82 DefaultShared ${if cfg.defaultShared then "Yes" else "No"}
83
84 Browsing ${if cfg.browsing then "Yes" else "No"}
85
86 WebInterface ${if cfg.webInterface then "Yes" else "No"}
87
88 LogLevel ${cfg.logLevel}
89
90 ${cfg.extraConf}
91 '';
92
93 browsedFile = writeConf "cups-browsed.conf" cfg.browsedConf;
94
95 rootdir = pkgs.buildEnv {
96 name = "cups-progs";
97 paths = [
98 cupsFilesFile
99 cupsdFile
100 (writeConf "client.conf" cfg.clientConf)
101 (writeConf "snmp.conf" cfg.snmpConf)
102 ] ++ optional avahiEnabled browsedFile
103 ++ cfg.drivers;
104 pathsToLink = [ "/etc/cups" ];
105 ignoreCollisions = true;
106 };
107
108 filterGutenprint = filter (pkg: pkg.meta.isGutenprint or false == true);
109 containsGutenprint = pkgs: length (filterGutenprint pkgs) > 0;
110 getGutenprint = pkgs: head (filterGutenprint pkgs);
111
112 parsePorts = addresses: let
113 splitAddress = addr: strings.splitString ":" addr;
114 extractPort = addr: builtins.foldl' (a: b: b) "" (splitAddress addr);
115 in
116 builtins.map (address: strings.toInt (extractPort address)) addresses;
117
118in
119
120{
121
122 imports = [
123 (mkChangedOptionModule [ "services" "printing" "gutenprint" ] [ "services" "printing" "drivers" ]
124 (config:
125 let enabled = getAttrFromPath [ "services" "printing" "gutenprint" ] config;
126 in if enabled then [ pkgs.gutenprint ] else [ ]))
127 (mkRemovedOptionModule [ "services" "printing" "cupsFilesConf" ] "")
128 (mkRemovedOptionModule [ "services" "printing" "cupsdConf" ] "")
129 ];
130
131 ###### interface
132
133 options = {
134 services.printing = {
135
136 enable = mkOption {
137 type = types.bool;
138 default = false;
139 description = ''
140 Whether to enable printing support through the CUPS daemon.
141 '';
142 };
143
144 package = lib.mkPackageOption pkgs "cups" {};
145
146 stateless = mkOption {
147 type = types.bool;
148 default = false;
149 description = ''
150 If set, all state directories relating to CUPS will be removed on
151 startup of the service.
152 '';
153 };
154
155 startWhenNeeded = mkOption {
156 type = types.bool;
157 default = true;
158 description = ''
159 If set, CUPS is socket-activated; that is,
160 instead of having it permanently running as a daemon,
161 systemd will start it on the first incoming connection.
162 '';
163 };
164
165 listenAddresses = mkOption {
166 type = types.listOf types.str;
167 default = [ "localhost:631" ];
168 example = [ "*:631" ];
169 description = ''
170 A list of addresses and ports on which to listen.
171 '';
172 };
173
174 allowFrom = mkOption {
175 type = types.listOf types.str;
176 default = [ "localhost" ];
177 example = [ "all" ];
178 apply = concatMapStringsSep "\n" (x: "Allow ${x}");
179 description = ''
180 From which hosts to allow unconditional access.
181 '';
182 };
183
184 openFirewall = mkOption {
185 type = types.bool;
186 default = false;
187 description = ''
188 Whether to open the firewall for TCP/UDP ports specified in
189 listenAdrresses option.
190 '';
191 };
192
193 bindirCmds = mkOption {
194 type = types.lines;
195 internal = true;
196 default = "";
197 description = ''
198 Additional commands executed while creating the directory
199 containing the CUPS server binaries.
200 '';
201 };
202
203 defaultShared = mkOption {
204 type = types.bool;
205 default = false;
206 description = ''
207 Specifies whether local printers are shared by default.
208 '';
209 };
210
211 browsing = mkOption {
212 type = types.bool;
213 default = false;
214 description = ''
215 Specifies whether shared printers are advertised.
216 '';
217 };
218
219 webInterface = mkOption {
220 type = types.bool;
221 default = true;
222 description = ''
223 Specifies whether the web interface is enabled.
224 '';
225 };
226
227 logLevel = mkOption {
228 type = types.str;
229 default = "info";
230 example = "debug";
231 description = ''
232 Specifies the cupsd logging verbosity.
233 '';
234 };
235
236 extraFilesConf = mkOption {
237 type = types.lines;
238 default = "";
239 description = ''
240 Extra contents of the configuration file of the CUPS daemon
241 ({file}`cups-files.conf`).
242 '';
243 };
244
245 extraConf = mkOption {
246 type = types.lines;
247 default = "";
248 example =
249 ''
250 BrowsePoll cups.example.com
251 MaxCopies 42
252 '';
253 description = ''
254 Extra contents of the configuration file of the CUPS daemon
255 ({file}`cupsd.conf`).
256 '';
257 };
258
259 clientConf = mkOption {
260 type = types.lines;
261 default = "";
262 example =
263 ''
264 ServerName server.example.com
265 Encryption Never
266 '';
267 description = ''
268 The contents of the client configuration.
269 ({file}`client.conf`)
270 '';
271 };
272
273 browsedConf = mkOption {
274 type = types.lines;
275 default = "";
276 example =
277 ''
278 BrowsePoll cups.example.com
279 '';
280 description = ''
281 The contents of the configuration. file of the CUPS Browsed daemon
282 ({file}`cups-browsed.conf`)
283 '';
284 };
285
286 snmpConf = mkOption {
287 type = types.lines;
288 default = ''
289 Address @LOCAL
290 '';
291 description = ''
292 The contents of {file}`/etc/cups/snmp.conf`. See "man
293 cups-snmp.conf" for a complete description.
294 '';
295 };
296
297 drivers = mkOption {
298 type = types.listOf types.path;
299 default = [];
300 example = literalExpression "with pkgs; [ gutenprint hplip splix ]";
301 description = ''
302 CUPS drivers to use. Drivers provided by CUPS, cups-filters,
303 Ghostscript and Samba are added unconditionally. If this list contains
304 Gutenprint (i.e. a derivation with
305 `meta.isGutenprint = true`) the PPD files in
306 {file}`/var/lib/cups/ppd` will be updated automatically
307 to avoid errors due to incompatible versions.
308 '';
309 };
310
311 tempDir = mkOption {
312 type = types.path;
313 default = "/tmp";
314 example = "/tmp/cups";
315 description = ''
316 CUPSd temporary directory.
317 '';
318 };
319 };
320
321 };
322
323
324 ###### implementation
325
326 config = mkIf config.services.printing.enable {
327
328 users.users.cups =
329 { uid = config.ids.uids.cups;
330 group = "lp";
331 description = "CUPS printing services";
332 };
333
334 # We need xdg-open (part of xdg-utils) for the desktop-file to proper open the users default-browser when opening "Manage Printing"
335 # https://github.com/NixOS/nixpkgs/pull/237994#issuecomment-1597510969
336 environment.systemPackages = [ cups.out xdg-utils ] ++ optional polkitEnabled cups-pk-helper;
337 environment.etc.cups.source = "/var/lib/cups";
338
339 services.dbus.packages = [ cups.out ] ++ optional polkitEnabled cups-pk-helper;
340 services.udev.packages = cfg.drivers;
341
342 # Allow asswordless printer admin for members of wheel group
343 security.polkit.extraConfig = mkIf polkitEnabled ''
344 polkit.addRule(function(action, subject) {
345 if (action.id == "org.opensuse.cupspkhelper.mechanism.all-edit" &&
346 subject.isInGroup("wheel")){
347 return polkit.Result.YES;
348 }
349 });
350 '';
351
352 # Cups uses libusb to talk to printers, and does not use the
353 # linux kernel driver. If the driver is not in a black list, it
354 # gets loaded, and then cups cannot access the printers.
355 boot.blacklistedKernelModules = [ "usblp" ];
356
357 # Some programs like print-manager rely on this value to get
358 # printer test pages.
359 environment.sessionVariables.CUPS_DATADIR = "${bindir}/share/cups";
360
361 systemd.packages = [ cups.out ];
362
363 systemd.sockets.cups = mkIf cfg.startWhenNeeded {
364 wantedBy = [ "sockets.target" ];
365 listenStreams = [ "" "/run/cups/cups.sock" ]
366 ++ map (x: replaceStrings ["localhost"] ["127.0.0.1"] (removePrefix "*:" x)) cfg.listenAddresses;
367 };
368
369 systemd.services.cups =
370 { wantedBy = optionals (!cfg.startWhenNeeded) [ "multi-user.target" ];
371 wants = [ "network.target" ];
372 after = [ "network.target" ];
373
374 path = [ cups.out ];
375
376 preStart = lib.optionalString cfg.stateless ''
377 rm -rf /var/cache/cups /var/lib/cups /var/spool/cups
378 '' + ''
379 mkdir -m 0700 -p /var/cache/cups
380 mkdir -m 0700 -p /var/spool/cups
381 mkdir -m 0755 -p ${cfg.tempDir}
382
383 mkdir -m 0755 -p /var/lib/cups
384 # While cups will automatically create self-signed certificates if accessed via TLS,
385 # this directory to store the certificates needs to be created manually.
386 mkdir -m 0700 -p /var/lib/cups/ssl
387
388 # Backwards compatibility
389 if [ ! -L /etc/cups ]; then
390 mv /etc/cups/* /var/lib/cups
391 rmdir /etc/cups
392 ln -s /var/lib/cups /etc/cups
393 fi
394 # First, clean existing symlinks
395 if [ -n "$(ls /var/lib/cups)" ]; then
396 for i in /var/lib/cups/*; do
397 [ -L "$i" ] && rm "$i"
398 done
399 fi
400 # Then, populate it with static files
401 cd ${rootdir}/etc/cups
402 for i in *; do
403 [ ! -e "/var/lib/cups/$i" ] && ln -s "${rootdir}/etc/cups/$i" "/var/lib/cups/$i"
404 done
405
406 #update path reference
407 [ -L /var/lib/cups/path ] && \
408 rm /var/lib/cups/path
409 [ ! -e /var/lib/cups/path ] && \
410 ln -s ${bindir} /var/lib/cups/path
411
412 ${optionalString (containsGutenprint cfg.drivers) ''
413 if [ -d /var/lib/cups/ppd ]; then
414 ${getGutenprint cfg.drivers}/bin/cups-genppdupdate -p /var/lib/cups/ppd
415 fi
416 ''}
417 '';
418
419 serviceConfig.PrivateTmp = true;
420 };
421
422 systemd.services.cups-browsed = mkIf avahiEnabled
423 { description = "CUPS Remote Printer Discovery";
424
425 wantedBy = [ "multi-user.target" ];
426 wants = [ "avahi-daemon.service" ] ++ optional (!cfg.startWhenNeeded) "cups.service";
427 bindsTo = [ "avahi-daemon.service" ] ++ optional (!cfg.startWhenNeeded) "cups.service";
428 partOf = [ "avahi-daemon.service" ] ++ optional (!cfg.startWhenNeeded) "cups.service";
429 after = [ "avahi-daemon.service" ] ++ optional (!cfg.startWhenNeeded) "cups.service";
430
431 path = [ cups ];
432
433 serviceConfig.ExecStart = "${cups-filters}/bin/cups-browsed";
434
435 restartTriggers = [ browsedFile ];
436 };
437
438 services.printing.extraConf =
439 ''
440 DefaultAuthType Basic
441
442 <Location />
443 Order allow,deny
444 ${cfg.allowFrom}
445 </Location>
446
447 <Location /admin>
448 Order allow,deny
449 ${cfg.allowFrom}
450 </Location>
451
452 <Location /admin/conf>
453 AuthType Basic
454 Require user @SYSTEM
455 Order allow,deny
456 ${cfg.allowFrom}
457 </Location>
458
459 <Policy default>
460 <Limit Send-Document Send-URI Hold-Job Release-Job Restart-Job Purge-Jobs Set-Job-Attributes Create-Job-Subscription Renew-Subscription Cancel-Subscription Get-Notifications Reprocess-Job Cancel-Current-Job Suspend-Current-Job Resume-Job CUPS-Move-Job>
461 Require user @OWNER @SYSTEM
462 Order deny,allow
463 </Limit>
464
465 <Limit Pause-Printer Resume-Printer Set-Printer-Attributes Enable-Printer Disable-Printer Pause-Printer-After-Current-Job Hold-New-Jobs Release-Held-New-Jobs Deactivate-Printer Activate-Printer Restart-Printer Shutdown-Printer Startup-Printer Promote-Job Schedule-Job-After CUPS-Add-Printer CUPS-Delete-Printer CUPS-Add-Class CUPS-Delete-Class CUPS-Accept-Jobs CUPS-Reject-Jobs CUPS-Set-Default>
466 AuthType Basic
467 Require user @SYSTEM
468 Order deny,allow
469 </Limit>
470
471 <Limit Cancel-Job CUPS-Authenticate-Job>
472 Require user @OWNER @SYSTEM
473 Order deny,allow
474 </Limit>
475
476 <Limit All>
477 Order deny,allow
478 </Limit>
479 </Policy>
480 '';
481
482 security.pam.services.cups = {};
483
484 networking.firewall = let
485 listenPorts = parsePorts cfg.listenAddresses;
486 in mkIf cfg.openFirewall {
487 allowedTCPPorts = listenPorts;
488 allowedUDPPorts = listenPorts;
489 };
490
491 };
492
493 meta.maintainers = with lib.maintainers; [ matthewbauer ];
494
495}