···
1
+
{ config, lib, pkgs, ... }:
5
+
# cups calls its backends as user `lp` (which is good!),
6
+
# but cups-pdf wants to be called as `root`, so it can change ownership of files.
7
+
# We add a suid wrapper and a wrapper script to trick cups into calling the suid wrapper.
8
+
# Note that a symlink to the suid wrapper alone wouldn't suffice, cups would complain
9
+
# > File "/nix/store/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-cups-progs/lib/cups/backend/cups-pdf" has insecure permissions (0104554/uid=0/gid=20)
11
+
# wrapper script that redirects calls to the suid wrapper
12
+
cups-pdf-wrapper = pkgs.writeTextFile {
13
+
name = "${pkgs.cups-pdf-to-pdf.name}-wrapper.sh";
15
+
destination = "/lib/cups/backend/cups-pdf";
17
+
${pkgs.stdenv.shellDryRun} "$target"
18
+
${lib.getExe pkgs.shellcheck} "$target"
21
+
#! ${pkgs.runtimeShell}
22
+
exec "${config.security.wrapperDir}/cups-pdf" "$@"
26
+
# wrapped cups-pdf package that uses the suid wrapper
27
+
cups-pdf-wrapped = pkgs.buildEnv {
28
+
name = "${pkgs.cups-pdf-to-pdf.name}-wrapped";
29
+
# using the wrapper as first path ensures it is used
30
+
paths = [ cups-pdf-wrapper pkgs.cups-pdf-to-pdf ];
31
+
ignoreCollisions = true;
34
+
instanceSettings = name: {
35
+
freeformType = with lib.types; nullOr (oneOf [ int str path package ]);
36
+
# override defaults:
37
+
# inject instance name into paths,
38
+
# also avoid conflicts between user names and special dirs
39
+
options.Out = lib.mkOption {
40
+
type = with lib.types; nullOr singleLineStr;
41
+
default = "/var/spool/cups-pdf-${name}/users/\${USER}";
42
+
defaultText = "/var/spool/cups-pdf-{instance-name}/users/\${USER}";
43
+
example = "\${HOME}/cups-pdf";
44
+
description = lib.mdDoc ''
46
+
`''${HOME}` will be expanded to the user's home directory,
47
+
`''${USER}` will be expanded to the user name.
50
+
options.AnonDirName = lib.mkOption {
51
+
type = with lib.types; nullOr singleLineStr;
52
+
default = "/var/spool/cups-pdf-${name}/anonymous";
53
+
defaultText = "/var/spool/cups-pdf-{instance-name}/anonymous";
54
+
example = "/var/lib/cups-pdf";
55
+
description = lib.mdDoc "path for anonymously created PDF files";
57
+
options.Spool = lib.mkOption {
58
+
type = with lib.types; nullOr singleLineStr;
59
+
default = "/var/spool/cups-pdf-${name}/spool";
60
+
defaultText = "/var/spool/cups-pdf-{instance-name}/spool";
61
+
example = "/var/lib/cups-pdf";
62
+
description = lib.mdDoc "spool directory";
64
+
options.Anonuser = lib.mkOption {
65
+
type = lib.types.singleLineStr;
67
+
description = lib.mdDoc ''
68
+
User for anonymous PDF creation.
69
+
An empty string disables this feature.
72
+
options.GhostScript = lib.mkOption {
73
+
type = with lib.types; nullOr path;
74
+
default = lib.getExe pkgs.ghostscript;
75
+
defaultText = lib.literalExpression "lib.getExe pkgs.ghostscript";
76
+
example = lib.literalExpression ''''${pkgs.ghostscript}/bin/ps2pdf'';
77
+
description = lib.mdDoc "location of GhostScript binary";
81
+
instanceConfig = { name, config, ... }: {
83
+
enable = (lib.mkEnableOption (lib.mdDoc "this cups-pdf instance")) // { default = true; };
84
+
installPrinter = (lib.mkEnableOption (lib.mdDoc ''
85
+
a CUPS printer queue for this instance.
86
+
The queue will be named after the instance and will use the {file}`CUPS-PDF_opt.ppd` ppd file.
87
+
If this is disabled, you need to add the queue yourself to use the instance
88
+
'')) // { default = true; };
89
+
confFileText = lib.mkOption {
90
+
type = lib.types.lines;
91
+
description = lib.mdDoc ''
92
+
This will contain the contents of {file}`cups-pdf.conf` for this instance, derived from {option}`settings`.
93
+
You can use this option to append text to the file.
96
+
settings = lib.mkOption {
97
+
type = lib.types.submodule (instanceSettings name);
100
+
Out = "\${HOME}/cups-pdf";
101
+
UserUMask = "0033";
103
+
description = lib.mdDoc ''
104
+
Settings for a cups-pdf instance, see the descriptions in the template config file in the cups-pdf package.
105
+
The key value pairs declared here will be translated into proper key value pairs for {file}`cups-pdf.conf`.
106
+
Setting a value to `null` disables the option and removes it from the file.
110
+
config.confFileText = lib.pipe config.settings [
111
+
(lib.filterAttrs (key: value: value != null))
112
+
(lib.mapAttrs (key: builtins.toString))
113
+
(lib.mapAttrsToList (key: value: "${key} ${value}\n"))
118
+
cupsPdfCfg = config.services.printing.cups-pdf;
120
+
copyConfigFileCmds = lib.pipe cupsPdfCfg.instances [
121
+
(lib.filterAttrs (name: lib.getAttr "enable"))
122
+
(lib.mapAttrs (name: lib.getAttr "confFileText"))
123
+
(lib.mapAttrs (name: pkgs.writeText "cups-pdf-${name}.conf"))
124
+
(lib.mapAttrsToList (name: confFile: "ln --symbolic --no-target-directory ${confFile} /var/lib/cups/cups-pdf-${name}.conf\n"))
128
+
printerSettings = lib.pipe cupsPdfCfg.instances [
129
+
(lib.filterAttrs (name: lib.getAttr "enable"))
130
+
(lib.filterAttrs (name: lib.getAttr "installPrinter"))
131
+
(lib.mapAttrsToList (name: instance: (lib.mapAttrs (key: lib.mkDefault) {
133
+
model = "CUPS-PDF_opt.ppd";
134
+
deviceUri = "cups-pdf:/${name}";
135
+
description = "virtual printer for cups-pdf instance ${name}";
136
+
location = instance.settings.Out;
144
+
options.services.printing.cups-pdf = {
145
+
enable = lib.mkEnableOption (lib.mdDoc ''
146
+
the cups-pdf virtual pdf printer backend.
147
+
By default, this will install a single printer `pdf`.
148
+
but this can be changed/extended with {option}`services.printing.cups-pdf.instances`
150
+
instances = lib.mkOption {
151
+
type = lib.types.attrsOf (lib.types.submodule instanceConfig);
153
+
example.pdf.settings = {
154
+
Out = "\${HOME}/cups-pdf";
155
+
UserUMask = "0033";
157
+
description = lib.mdDoc ''
158
+
Permits to raise one or more cups-pdf instances.
159
+
Each instance is named by an attribute name, and the attribute's values control the instance' configuration.
164
+
config = lib.mkIf cupsPdfCfg.enable {
165
+
services.printing.enable = true;
166
+
services.printing.drivers = [ cups-pdf-wrapped ];
167
+
hardware.printers.ensurePrinters = printerSettings;
168
+
# the cups module will install the default config file,
169
+
# but we don't need it and it would confuse cups-pdf
170
+
systemd.services.cups.preStart = lib.mkAfter ''
171
+
rm -f /var/lib/cups/cups-pdf.conf
172
+
${copyConfigFileCmds}
174
+
security.wrappers.cups-pdf = {
177
+
permissions = "+r,ug+x";
179
+
source = "${pkgs.cups-pdf-to-pdf}/lib/cups/backend/cups-pdf";
183
+
meta.maintainers = [ lib.maintainers.yarny ];