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