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