at master 7.3 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8let 9 10 # cups calls its backends as user `lp` (which is good!), 11 # but cups-pdf wants to be called as `root`, so it can change ownership of files. 12 # We add a suid wrapper and a wrapper script to trick cups into calling the suid wrapper. 13 # Note that a symlink to the suid wrapper alone wouldn't suffice, cups would complain 14 # > File "/nix/store/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-cups-progs/lib/cups/backend/cups-pdf" has insecure permissions (0104554/uid=0/gid=20) 15 16 # wrapper script that redirects calls to the suid wrapper 17 cups-pdf-wrapper = pkgs.writeTextFile { 18 name = "${pkgs.cups-pdf-to-pdf.name}-wrapper.sh"; 19 executable = true; 20 destination = "/lib/cups/backend/cups-pdf"; 21 checkPhase = '' 22 ${pkgs.stdenv.shellDryRun} "$target" 23 ${lib.getExe pkgs.shellcheck} "$target" 24 ''; 25 text = '' 26 #! ${pkgs.runtimeShell} 27 exec "${config.security.wrapperDir}/cups-pdf" "$@" 28 ''; 29 }; 30 31 # wrapped cups-pdf package that uses the suid wrapper 32 cups-pdf-wrapped = pkgs.buildEnv { 33 name = "${pkgs.cups-pdf-to-pdf.name}-wrapped"; 34 # using the wrapper as first path ensures it is used 35 paths = [ 36 cups-pdf-wrapper 37 pkgs.cups-pdf-to-pdf 38 ]; 39 ignoreCollisions = true; 40 }; 41 42 instanceSettings = name: { 43 freeformType = 44 with lib.types; 45 attrsOf ( 46 nullOr (oneOf [ 47 int 48 str 49 path 50 package 51 ]) 52 ); 53 # override defaults: 54 # inject instance name into paths, 55 # also avoid conflicts between user names and special dirs 56 options.Out = lib.mkOption { 57 type = with lib.types; nullOr singleLineStr; 58 default = "/var/spool/cups-pdf-${name}/users/\${USER}"; 59 defaultText = "/var/spool/cups-pdf-{instance-name}/users/\${USER}"; 60 example = "\${HOME}/cups-pdf"; 61 description = '' 62 output directory; 63 `''${HOME}` will be expanded to the user's home directory, 64 `''${USER}` will be expanded to the user name. 65 ''; 66 }; 67 options.AnonDirName = lib.mkOption { 68 type = with lib.types; nullOr singleLineStr; 69 default = "/var/spool/cups-pdf-${name}/anonymous"; 70 defaultText = "/var/spool/cups-pdf-{instance-name}/anonymous"; 71 example = "/var/lib/cups-pdf"; 72 description = "path for anonymously created PDF files"; 73 }; 74 options.Spool = lib.mkOption { 75 type = with lib.types; nullOr singleLineStr; 76 default = "/var/spool/cups-pdf-${name}/spool"; 77 defaultText = "/var/spool/cups-pdf-{instance-name}/spool"; 78 example = "/var/lib/cups-pdf"; 79 description = "spool directory"; 80 }; 81 options.Anonuser = lib.mkOption { 82 type = lib.types.singleLineStr; 83 default = "root"; 84 description = '' 85 User for anonymous PDF creation. 86 An empty string disables this feature. 87 ''; 88 }; 89 options.GhostScript = lib.mkOption { 90 type = with lib.types; nullOr path; 91 default = lib.getExe pkgs.ghostscript; 92 defaultText = lib.literalExpression "lib.getExe pkgs.ghostscript"; 93 example = lib.literalExpression ''''${pkgs.ghostscript}/bin/ps2pdf''; 94 description = "location of GhostScript binary"; 95 }; 96 }; 97 98 instanceConfig = 99 { name, config, ... }: 100 { 101 options = { 102 enable = (lib.mkEnableOption "this cups-pdf instance") // { 103 default = true; 104 }; 105 installPrinter = 106 (lib.mkEnableOption '' 107 a CUPS printer queue for this instance. 108 The queue will be named after the instance and will use the {file}`CUPS-PDF_opt.ppd` ppd file. 109 If this is disabled, you need to add the queue yourself to use the instance 110 '') 111 // { 112 default = true; 113 }; 114 confFileText = lib.mkOption { 115 type = lib.types.lines; 116 description = '' 117 This will contain the contents of {file}`cups-pdf.conf` for this instance, derived from {option}`settings`. 118 You can use this option to append text to the file. 119 ''; 120 }; 121 settings = lib.mkOption { 122 type = lib.types.submodule (instanceSettings name); 123 default = { }; 124 example = { 125 Out = "\${HOME}/cups-pdf"; 126 UserUMask = "0033"; 127 }; 128 description = '' 129 Settings for a cups-pdf instance, see the descriptions in the template config file in the cups-pdf package. 130 The key value pairs declared here will be translated into proper key value pairs for {file}`cups-pdf.conf`. 131 Setting a value to `null` disables the option and removes it from the file. 132 ''; 133 }; 134 }; 135 config.confFileText = lib.pipe config.settings [ 136 (lib.filterAttrs (key: value: value != null)) 137 (lib.mapAttrs (key: builtins.toString)) 138 (lib.mapAttrsToList (key: value: "${key} ${value}\n")) 139 lib.concatStrings 140 ]; 141 }; 142 143 cupsPdfCfg = config.services.printing.cups-pdf; 144 145 copyConfigFileCmds = lib.pipe cupsPdfCfg.instances [ 146 (lib.filterAttrs (name: lib.getAttr "enable")) 147 (lib.mapAttrs (name: lib.getAttr "confFileText")) 148 (lib.mapAttrs (name: pkgs.writeText "cups-pdf-${name}.conf")) 149 (lib.mapAttrsToList ( 150 name: confFile: 151 "ln --symbolic --no-target-directory ${confFile} /var/lib/cups/cups-pdf-${name}.conf\n" 152 )) 153 lib.concatStrings 154 ]; 155 156 printerSettings = lib.pipe cupsPdfCfg.instances [ 157 (lib.filterAttrs (name: lib.getAttr "enable")) 158 (lib.filterAttrs (name: lib.getAttr "installPrinter")) 159 (lib.mapAttrsToList ( 160 name: instance: 161 (lib.mapAttrs (key: lib.mkDefault) { 162 inherit name; 163 model = "CUPS-PDF_opt.ppd"; 164 deviceUri = "cups-pdf:/${name}"; 165 description = "virtual printer for cups-pdf instance ${name}"; 166 location = instance.settings.Out; 167 }) 168 )) 169 ]; 170 171in 172 173{ 174 175 options.services.printing.cups-pdf = { 176 enable = lib.mkEnableOption '' 177 the cups-pdf virtual pdf printer backend. 178 By default, this will install a single printer `pdf`. 179 but this can be changed/extended with {option}`services.printing.cups-pdf.instances` 180 ''; 181 instances = lib.mkOption { 182 type = lib.types.attrsOf (lib.types.submodule instanceConfig); 183 default.pdf = { }; 184 example.pdf.settings = { 185 Out = "\${HOME}/cups-pdf"; 186 UserUMask = "0033"; 187 }; 188 description = '' 189 Permits to raise one or more cups-pdf instances. 190 Each instance is named by an attribute name, and the attribute's values control the instance' configuration. 191 ''; 192 }; 193 }; 194 195 config = lib.mkIf cupsPdfCfg.enable { 196 services.printing.enable = true; 197 services.printing.drivers = [ cups-pdf-wrapped ]; 198 hardware.printers.ensurePrinters = printerSettings; 199 # the cups module will install the default config file, 200 # but we don't need it and it would confuse cups-pdf 201 systemd.services.cups.preStart = lib.mkAfter '' 202 rm -f /var/lib/cups/cups-pdf.conf 203 ${copyConfigFileCmds} 204 ''; 205 security.wrappers.cups-pdf = { 206 group = "lp"; 207 owner = "root"; 208 permissions = "+r,ug+x"; 209 setuid = true; 210 source = "${pkgs.cups-pdf-to-pdf}/lib/cups/backend/cups-pdf"; 211 }; 212 }; 213 214 meta.maintainers = [ lib.maintainers.yarny ]; 215 216}