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 = ''
133 Whether to enable printing support through the CUPS daemon.
134 '';
135 };
136
137 startWhenNeeded = mkOption {
138 type = types.bool;
139 default = true;
140 description = ''
141 If set, CUPS is socket-activated; that is,
142 instead of having it permanently running as a daemon,
143 systemd will start it on the first incoming connection.
144 '';
145 };
146
147 listenAddresses = mkOption {
148 type = types.listOf types.str;
149 default = [ "localhost:631" ];
150 example = [ "*:631" ];
151 description = ''
152 A list of addresses and ports on which to listen.
153 '';
154 };
155
156 allowFrom = mkOption {
157 type = types.listOf types.str;
158 default = [ "localhost" ];
159 example = [ "all" ];
160 apply = concatMapStringsSep "\n" (x: "Allow ${x}");
161 description = ''
162 From which hosts to allow unconditional access.
163 '';
164 };
165
166 bindirCmds = mkOption {
167 type = types.lines;
168 internal = true;
169 default = "";
170 description = ''
171 Additional commands executed while creating the directory
172 containing the CUPS server binaries.
173 '';
174 };
175
176 defaultShared = mkOption {
177 type = types.bool;
178 default = false;
179 description = ''
180 Specifies whether local printers are shared by default.
181 '';
182 };
183
184 browsing = mkOption {
185 type = types.bool;
186 default = false;
187 description = ''
188 Specifies whether shared printers are advertised.
189 '';
190 };
191
192 webInterface = mkOption {
193 type = types.bool;
194 default = true;
195 description = ''
196 Specifies whether the web interface is enabled.
197 '';
198 };
199
200 logLevel = mkOption {
201 type = types.str;
202 default = "info";
203 example = "debug";
204 description = ''
205 Specifies the cupsd logging verbosity.
206 '';
207 };
208
209 extraFilesConf = mkOption {
210 type = types.lines;
211 default = "";
212 description = ''
213 Extra contents of the configuration file of the CUPS daemon
214 (<filename>cups-files.conf</filename>).
215 '';
216 };
217
218 extraConf = mkOption {
219 type = types.lines;
220 default = "";
221 example =
222 ''
223 BrowsePoll cups.example.com
224 MaxCopies 42
225 '';
226 description = ''
227 Extra contents of the configuration file of the CUPS daemon
228 (<filename>cupsd.conf</filename>).
229 '';
230 };
231
232 clientConf = mkOption {
233 type = types.lines;
234 default = "";
235 example =
236 ''
237 ServerName server.example.com
238 Encryption Never
239 '';
240 description = ''
241 The contents of the client configuration.
242 (<filename>client.conf</filename>)
243 '';
244 };
245
246 browsedConf = mkOption {
247 type = types.lines;
248 default = "";
249 example =
250 ''
251 BrowsePoll cups.example.com
252 '';
253 description = ''
254 The contents of the configuration. file of the CUPS Browsed daemon
255 (<filename>cups-browsed.conf</filename>)
256 '';
257 };
258
259 snmpConf = mkOption {
260 type = types.lines;
261 default = ''
262 Address @LOCAL
263 '';
264 description = ''
265 The contents of <filename>/etc/cups/snmp.conf</filename>. See "man
266 cups-snmp.conf" for a complete description.
267 '';
268 };
269
270 drivers = mkOption {
271 type = types.listOf types.path;
272 default = [];
273 example = literalExpression "with pkgs; [ gutenprint hplip splix ]";
274 description = ''
275 CUPS drivers to use. Drivers provided by CUPS, cups-filters,
276 Ghostscript and Samba are added unconditionally. If this list contains
277 Gutenprint (i.e. a derivation with
278 <literal>meta.isGutenprint = true</literal>) the PPD files in
279 <filename>/var/lib/cups/ppd</filename> will be updated automatically
280 to avoid errors due to incompatible versions.
281 '';
282 };
283
284 tempDir = mkOption {
285 type = types.path;
286 default = "/tmp";
287 example = "/tmp/cups";
288 description = ''
289 CUPSd temporary directory.
290 '';
291 };
292 };
293
294 };
295
296
297 ###### implementation
298
299 config = mkIf config.services.printing.enable {
300
301 users.users.cups =
302 { uid = config.ids.uids.cups;
303 group = "lp";
304 description = "CUPS printing services";
305 };
306
307 environment.systemPackages = [ cups.out ] ++ optional polkitEnabled cups-pk-helper;
308 environment.etc.cups.source = "/var/lib/cups";
309
310 services.dbus.packages = [ cups.out ] ++ optional polkitEnabled cups-pk-helper;
311
312 # Allow asswordless printer admin for members of wheel group
313 security.polkit.extraConfig = mkIf polkitEnabled ''
314 polkit.addRule(function(action, subject) {
315 if (action.id == "org.opensuse.cupspkhelper.mechanism.all-edit" &&
316 subject.isInGroup("wheel")){
317 return polkit.Result.YES;
318 }
319 });
320 '';
321
322 # Cups uses libusb to talk to printers, and does not use the
323 # linux kernel driver. If the driver is not in a black list, it
324 # gets loaded, and then cups cannot access the printers.
325 boot.blacklistedKernelModules = [ "usblp" ];
326
327 # Some programs like print-manager rely on this value to get
328 # printer test pages.
329 environment.sessionVariables.CUPS_DATADIR = "${bindir}/share/cups";
330
331 systemd.packages = [ cups.out ];
332
333 systemd.sockets.cups = mkIf cfg.startWhenNeeded {
334 wantedBy = [ "sockets.target" ];
335 listenStreams = [ "/run/cups/cups.sock" ]
336 ++ map (x: replaceStrings ["localhost"] ["127.0.0.1"] (removePrefix "*:" x)) cfg.listenAddresses;
337 };
338
339 systemd.services.cups =
340 { wantedBy = optionals (!cfg.startWhenNeeded) [ "multi-user.target" ];
341 wants = [ "network.target" ];
342 after = [ "network.target" ];
343
344 path = [ cups.out ];
345
346 preStart =
347 ''
348 mkdir -m 0700 -p /var/cache/cups
349 mkdir -m 0700 -p /var/spool/cups
350 mkdir -m 0755 -p ${cfg.tempDir}
351
352 mkdir -m 0755 -p /var/lib/cups
353 # While cups will automatically create self-signed certificates if accessed via TLS,
354 # this directory to store the certificates needs to be created manually.
355 mkdir -m 0700 -p /var/lib/cups/ssl
356
357 # Backwards compatibility
358 if [ ! -L /etc/cups ]; then
359 mv /etc/cups/* /var/lib/cups
360 rmdir /etc/cups
361 ln -s /var/lib/cups /etc/cups
362 fi
363 # First, clean existing symlinks
364 if [ -n "$(ls /var/lib/cups)" ]; then
365 for i in /var/lib/cups/*; do
366 [ -L "$i" ] && rm "$i"
367 done
368 fi
369 # Then, populate it with static files
370 cd ${rootdir}/etc/cups
371 for i in *; do
372 [ ! -e "/var/lib/cups/$i" ] && ln -s "${rootdir}/etc/cups/$i" "/var/lib/cups/$i"
373 done
374
375 #update path reference
376 [ -L /var/lib/cups/path ] && \
377 rm /var/lib/cups/path
378 [ ! -e /var/lib/cups/path ] && \
379 ln -s ${bindir} /var/lib/cups/path
380
381 ${optionalString (containsGutenprint cfg.drivers) ''
382 if [ -d /var/lib/cups/ppd ]; then
383 ${getGutenprint cfg.drivers}/bin/cups-genppdupdate -p /var/lib/cups/ppd
384 fi
385 ''}
386 '';
387
388 serviceConfig = {
389 PrivateTmp = true;
390 RuntimeDirectory = [ "cups" ];
391 };
392 };
393
394 systemd.services.cups-browsed = mkIf avahiEnabled
395 { description = "CUPS Remote Printer Discovery";
396
397 wantedBy = [ "multi-user.target" ];
398 wants = [ "avahi-daemon.service" ] ++ optional (!cfg.startWhenNeeded) "cups.service";
399 bindsTo = [ "avahi-daemon.service" ] ++ optional (!cfg.startWhenNeeded) "cups.service";
400 partOf = [ "avahi-daemon.service" ] ++ optional (!cfg.startWhenNeeded) "cups.service";
401 after = [ "avahi-daemon.service" ] ++ optional (!cfg.startWhenNeeded) "cups.service";
402
403 path = [ cups ];
404
405 serviceConfig.ExecStart = "${cups-filters}/bin/cups-browsed";
406
407 restartTriggers = [ browsedFile ];
408 };
409
410 services.printing.extraConf =
411 ''
412 DefaultAuthType Basic
413
414 <Location />
415 Order allow,deny
416 ${cfg.allowFrom}
417 </Location>
418
419 <Location /admin>
420 Order allow,deny
421 ${cfg.allowFrom}
422 </Location>
423
424 <Location /admin/conf>
425 AuthType Basic
426 Require user @SYSTEM
427 Order allow,deny
428 ${cfg.allowFrom}
429 </Location>
430
431 <Policy default>
432 <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>
433 Require user @OWNER @SYSTEM
434 Order deny,allow
435 </Limit>
436
437 <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>
438 AuthType Basic
439 Require user @SYSTEM
440 Order deny,allow
441 </Limit>
442
443 <Limit Cancel-Job CUPS-Authenticate-Job>
444 Require user @OWNER @SYSTEM
445 Order deny,allow
446 </Limit>
447
448 <Limit All>
449 Order deny,allow
450 </Limit>
451 </Policy>
452 '';
453
454 security.pam.services.cups = {};
455
456 };
457
458 meta.maintainers = with lib.maintainers; [ matthewbauer ];
459
460}