#587 Implement SHB module for paperless service
This commit is contained in:
parent
879254bcfd
commit
d655655e0c
3 changed files with 630 additions and 0 deletions
|
|
@ -302,6 +302,7 @@
|
|||
// (vm_test "karakeep" ./test/services/karakeep.nix)
|
||||
// (vm_test "nextcloud" ./test/services/nextcloud.nix)
|
||||
// (vm_test "open-webui" ./test/services/open-webui.nix)
|
||||
// (vm_test "paperless" ./test/services/paperless.nix)
|
||||
// (vm_test "pinchflat" ./test/services/pinchflat.nix)
|
||||
// (vm_test "vaultwarden" ./test/services/vaultwarden.nix)
|
||||
|
||||
|
|
@ -410,6 +411,7 @@
|
|||
self.nixosModules.nextcloud-server
|
||||
self.nixosModules.open-webui
|
||||
self.nixosModules.pinchflat
|
||||
self.nixosModules.paperless
|
||||
self.nixosModules.vaultwarden
|
||||
];
|
||||
};
|
||||
|
|
@ -442,6 +444,7 @@
|
|||
nixosModules.karakeep = modules/services/karakeep.nix;
|
||||
nixosModules.nextcloud-server = modules/services/nextcloud-server.nix;
|
||||
nixosModules.open-webui = modules/services/open-webui.nix;
|
||||
nixosModules.paperless = modules/services/paperless.nix;
|
||||
nixosModules.pinchflat = modules/services/pinchflat.nix;
|
||||
nixosModules.vaultwarden = modules/services/vaultwarden.nix;
|
||||
};
|
||||
|
|
|
|||
447
modules/services/paperless.nix
Normal file
447
modules/services/paperless.nix
Normal file
|
|
@ -0,0 +1,447 @@
|
|||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.shb.paperless;
|
||||
contracts = pkgs.callPackage ../contracts { };
|
||||
dataFolder = cfg.dataDir;
|
||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||
protocol = if !(isNull cfg.ssl) then "https" else "http";
|
||||
ssoFqdnWithPort =
|
||||
if isNull cfg.sso.port then cfg.sso.endpoint else "${cfg.sso.endpoint}:${toString cfg.sso.port}";
|
||||
|
||||
ssoClientSettings = {
|
||||
openid_connect = {
|
||||
SCOPE = [
|
||||
"openid"
|
||||
"profile"
|
||||
"email"
|
||||
"groups"
|
||||
];
|
||||
OAUTH_PKCE_ENABLED = true;
|
||||
APPS = [
|
||||
{
|
||||
provider_id = "${cfg.sso.provider}";
|
||||
name = "${cfg.sso.provider}";
|
||||
client_id = "${cfg.sso.clientID}";
|
||||
secret = "%SECRET_CLIENT_SECRET_PLACEHOLDER%";
|
||||
settings = {
|
||||
server_url = ssoFqdnWithPort;
|
||||
token_auth_method = "client_secret_basic";
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
ssoClientSettingsFile = pkgs.writeText "paperless-sso-client.env" ''
|
||||
PAPERLESS_SOCIALACCOUNT_PROVIDERS=${builtins.toJSON ssoClientSettings}
|
||||
'';
|
||||
replacements = [
|
||||
{
|
||||
# Note: replaceSecretsScript prepends '%SECRET_' and appends '%'
|
||||
# when doing the replacement
|
||||
name = [ "CLIENT_SECRET_PLACEHOLDER" ];
|
||||
source = cfg.sso.sharedSecret.result.path;
|
||||
}
|
||||
];
|
||||
replaceSecretsScript = lib.shb.replaceSecretsScript {
|
||||
file = ssoClientSettingsFile;
|
||||
resultPath = "/run/paperless/paperless-sso-client.env";
|
||||
inherit replacements;
|
||||
user = "paperless";
|
||||
};
|
||||
inherit (lib)
|
||||
mkEnableOption
|
||||
mkIf
|
||||
lists
|
||||
mkOption
|
||||
optionals
|
||||
;
|
||||
inherit (lib.types)
|
||||
attrs
|
||||
attrsOf
|
||||
bool
|
||||
enum
|
||||
listOf
|
||||
nullOr
|
||||
port
|
||||
submodule
|
||||
str
|
||||
path
|
||||
;
|
||||
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
../blocks/nginx.nix
|
||||
];
|
||||
|
||||
options.shb.paperless = {
|
||||
enable = mkEnableOption "selfhostblocks.paperless";
|
||||
|
||||
subdomain = mkOption {
|
||||
type = str;
|
||||
description = ''
|
||||
Subdomain under which paperless will be served.
|
||||
|
||||
```
|
||||
<subdomain>.<domain>
|
||||
```
|
||||
'';
|
||||
example = "photos";
|
||||
};
|
||||
|
||||
domain = mkOption {
|
||||
description = ''
|
||||
Domain under which paperless is served.
|
||||
|
||||
```
|
||||
<subdomain>.<domain>
|
||||
```
|
||||
'';
|
||||
type = str;
|
||||
example = "example.com";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
description = ''
|
||||
Port under which paperless will listen.
|
||||
'';
|
||||
type = port;
|
||||
default = 28981;
|
||||
};
|
||||
|
||||
ssl = mkOption {
|
||||
description = "Path to SSL files";
|
||||
type = nullOr contracts.ssl.certs;
|
||||
default = null;
|
||||
};
|
||||
|
||||
dataDir = mkOption {
|
||||
description = "Directory where paperless will store data files.";
|
||||
type = str;
|
||||
default = "/var/lib/paperless";
|
||||
};
|
||||
|
||||
mediaDir = mkOption {
|
||||
description = "Directory where paperless will store documents.";
|
||||
type = str;
|
||||
defaultText = lib.literalExpression ''"''${dataDir}/media"'';
|
||||
default = "${cfg.dataDir}/media";
|
||||
};
|
||||
|
||||
consumptionDir = mkOption {
|
||||
description = "Directory from which new documents are imported.";
|
||||
type = str;
|
||||
defaultText = lib.literalExpression ''"''${dataDir}/consume"'';
|
||||
default = "${cfg.dataDir}/consume";
|
||||
};
|
||||
|
||||
configureTika = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether to configure Tika and Gotenberg to process Office and e-mail files with OCR.
|
||||
'';
|
||||
};
|
||||
|
||||
adminPassword = mkOption {
|
||||
description = "Secret containing the superuser (admin) password.";
|
||||
type = submodule {
|
||||
options = contracts.secret.mkRequester {
|
||||
mode = "0400";
|
||||
owner = "paperless";
|
||||
group = "paperless";
|
||||
restartUnits = [ "paperless-server.service" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
settings = lib.mkOption {
|
||||
type = lib.types.submodule {
|
||||
freeformType =
|
||||
with lib.types;
|
||||
attrsOf (
|
||||
let
|
||||
typeList = [
|
||||
bool
|
||||
float
|
||||
int
|
||||
str
|
||||
path
|
||||
package
|
||||
];
|
||||
in
|
||||
oneOf (
|
||||
typeList
|
||||
++ [
|
||||
(listOf (oneOf typeList))
|
||||
(attrsOf (oneOf typeList))
|
||||
]
|
||||
)
|
||||
);
|
||||
};
|
||||
default = { };
|
||||
description = ''
|
||||
Extra paperless config options.
|
||||
|
||||
See [the documentation](https://docs.paperless-ngx.com/configuration/) for available options.
|
||||
|
||||
Note that some settings such as `PAPERLESS_CONSUMER_IGNORE_PATTERN` expect JSON values.
|
||||
Settings declared as lists or attrsets will automatically be serialised into JSON strings for your convenience.
|
||||
'';
|
||||
example = {
|
||||
PAPERLESS_OCR_LANGUAGE = "deu+eng";
|
||||
PAPERLESS_CONSUMER_IGNORE_PATTERN = [
|
||||
".DS_STORE/*"
|
||||
"desktop.ini"
|
||||
];
|
||||
PAPERLESS_OCR_USER_ARGS = {
|
||||
optimize = 1;
|
||||
pdfa_image_compression = "lossless";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
mount = mkOption {
|
||||
type = contracts.mount;
|
||||
description = ''
|
||||
Mount configuration. This is an output option.
|
||||
|
||||
Use it to initialize a block implementing the "mount" contract.
|
||||
For example, with a zfs dataset:
|
||||
|
||||
```
|
||||
shb.zfs.datasets."paperless" = {
|
||||
poolName = "root";
|
||||
} // config.shb.paperless.mount;
|
||||
```
|
||||
'';
|
||||
readOnly = true;
|
||||
default = {
|
||||
path = dataFolder;
|
||||
};
|
||||
};
|
||||
|
||||
backup = mkOption {
|
||||
description = ''
|
||||
Backup configuration for paperless media files and database.
|
||||
'';
|
||||
default = { };
|
||||
type = submodule {
|
||||
options = contracts.backup.mkRequester {
|
||||
user = "paperless";
|
||||
sourceDirectories = [
|
||||
dataFolder
|
||||
];
|
||||
excludePatterns = [
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
sso = mkOption {
|
||||
description = ''
|
||||
Setup SSO integration.
|
||||
'';
|
||||
default = { };
|
||||
type = submodule {
|
||||
options = {
|
||||
enable = mkEnableOption "SSO integration.";
|
||||
|
||||
provider = mkOption {
|
||||
type = enum [
|
||||
"Authelia"
|
||||
"Keycloak"
|
||||
"Generic"
|
||||
];
|
||||
description = "OIDC provider name, used for display.";
|
||||
default = "Authelia";
|
||||
};
|
||||
|
||||
endpoint = mkOption {
|
||||
type = str;
|
||||
description = "OIDC endpoint for SSO.";
|
||||
example = "https://authelia.example.com";
|
||||
};
|
||||
|
||||
clientID = mkOption {
|
||||
type = str;
|
||||
description = "Client ID for the OIDC endpoint.";
|
||||
default = "paperless";
|
||||
};
|
||||
|
||||
adminUserGroup = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "OIDC admin group";
|
||||
default = "paperless_admin";
|
||||
};
|
||||
|
||||
userGroup = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "OIDC user group";
|
||||
default = "paperless_user";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
description = "If given, adds a port to the endpoint.";
|
||||
type = nullOr port;
|
||||
default = null;
|
||||
};
|
||||
|
||||
autoRegister = mkOption {
|
||||
type = bool;
|
||||
description = "Automatically register new users from SSO provider.";
|
||||
default = true;
|
||||
};
|
||||
|
||||
autoLaunch = mkOption {
|
||||
type = bool;
|
||||
description = "Automatically redirect to SSO provider.";
|
||||
default = true;
|
||||
};
|
||||
|
||||
passwordLogin = mkOption {
|
||||
type = bool;
|
||||
description = "Enable password login.";
|
||||
default = true;
|
||||
};
|
||||
|
||||
sharedSecret = mkOption {
|
||||
description = "OIDC shared secret for paperless.";
|
||||
type = submodule {
|
||||
options = contracts.secret.mkRequester {
|
||||
mode = "0400";
|
||||
owner = "paperless";
|
||||
group = "paperless";
|
||||
restartUnits = [ "paperless-server.service" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
sharedSecretForAuthelia = mkOption {
|
||||
description = "OIDC shared secret for Authelia. Content must be the same as `sharedSecret` option.";
|
||||
type = submodule {
|
||||
options = contracts.secret.mkRequester {
|
||||
mode = "0400";
|
||||
owner = "authelia";
|
||||
};
|
||||
};
|
||||
default = null;
|
||||
};
|
||||
|
||||
authorization_policy = mkOption {
|
||||
type = enum [
|
||||
"one_factor"
|
||||
"two_factor"
|
||||
];
|
||||
description = "Require one factor (password) or two factor (device) authentication.";
|
||||
default = "one_factor";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = !(isNull cfg.ssl) -> !(isNull cfg.ssl.paths.cert) && !(isNull cfg.ssl.paths.key);
|
||||
message = "SSL is enabled for paperless but no cert or key is provided.";
|
||||
}
|
||||
{
|
||||
assertion = cfg.sso.enable -> cfg.ssl != null;
|
||||
message = "To integrate SSO, SSL must be enabled, set the shb.paperless.ssl option.";
|
||||
}
|
||||
];
|
||||
|
||||
# Configure paperless service
|
||||
services.paperless = {
|
||||
enable = true;
|
||||
address = "127.0.0.1";
|
||||
port = cfg.port;
|
||||
consumptionDirIsPublic = true;
|
||||
dataDir = cfg.dataDir;
|
||||
mediaDir = cfg.mediaDir;
|
||||
consumptionDir = cfg.consumptionDir;
|
||||
configureTika = cfg.configureTika;
|
||||
settings = {
|
||||
PAPERLESS_URL = "${protocol}://${fqdn}";
|
||||
}
|
||||
// cfg.settings
|
||||
// lib.optionalAttrs (cfg.sso.enable) {
|
||||
PAPERLESS_APPS = "allauth.socialaccount.providers.openid_connect";
|
||||
PAPERLESS_SOCIAL_AUTO_SIGNUP = cfg.sso.autoRegister;
|
||||
PAPERLESS_SOCIAL_ACCOUNT_SYNC_GROUPS = true;
|
||||
PAPERLESS_DISABLE_REGULAR_LOGIN = !cfg.sso.passwordLogin;
|
||||
};
|
||||
}
|
||||
// lib.optionalAttrs (cfg.sso.enable) {
|
||||
environmentFile = "/run/paperless/paperless-sso-client.env";
|
||||
};
|
||||
|
||||
# Database defaults to local sqlite
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d ${cfg.dataDir} 0700 paperless paperless"
|
||||
"d ${cfg.consumptionDir} 0700 paperless paperless"
|
||||
"d ${cfg.mediaDir} 0700 paperless paperless"
|
||||
]
|
||||
++ lib.optionals cfg.sso.enable [ "d '/run/paperless' 0750 root root - -" ];
|
||||
|
||||
systemd.services.paperless-pre = lib.mkIf cfg.sso.enable {
|
||||
script = replaceSecretsScript;
|
||||
serviceConfig.Type = "oneshot";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
before = [ "paperless-scheduler.service" ];
|
||||
requiredBy = [ "paperless-scheduler.service" ];
|
||||
};
|
||||
|
||||
shb.nginx.vhosts = [
|
||||
{
|
||||
inherit (cfg) subdomain domain ssl;
|
||||
upstream = "http://127.0.0.1:${toString cfg.port}";
|
||||
autheliaRules = lib.mkIf (cfg.sso.enable) [
|
||||
{
|
||||
domain = fqdn;
|
||||
policy = cfg.sso.authorization_policy;
|
||||
subject = [
|
||||
"group:paperless_user"
|
||||
"group:paperless_admin"
|
||||
];
|
||||
}
|
||||
];
|
||||
authEndpoint = lib.mkIf (cfg.sso.enable) cfg.sso.endpoint;
|
||||
extraConfig = ''
|
||||
# See https://github.com/paperless-ngx/paperless-ngx/wiki/Using-a-Reverse-Proxy-with-Paperless-ngx#nginx
|
||||
proxy_redirect off;
|
||||
proxy_set_header X-Forwarded-Host $server_name;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin";
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
# Allow large uploads
|
||||
services.nginx.virtualHosts."${fqdn}".extraConfig = ''
|
||||
client_max_body_size 500M;
|
||||
'';
|
||||
|
||||
shb.authelia.oidcClients = lists.optionals (cfg.sso.enable && cfg.sso.provider == "Authelia") [
|
||||
{
|
||||
client_id = cfg.sso.clientID;
|
||||
client_name = "paperless";
|
||||
client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path;
|
||||
public = false;
|
||||
authorization_policy = cfg.sso.authorization_policy;
|
||||
token_endpoint_auth_method = "client_secret_basic";
|
||||
redirect_uris = [
|
||||
"${protocol}://${fqdn}/accounts/oidc/${cfg.sso.provider}/login/callback/"
|
||||
];
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
180
test/services/paperless.nix
Normal file
180
test/services/paperless.nix
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
{ pkgs, lib }:
|
||||
let
|
||||
subdomain = "p";
|
||||
domain = "example.com";
|
||||
|
||||
commonTestScript = lib.shb.accessScript {
|
||||
hasSSL = { node, ... }: !(isNull node.config.shb.paperless.ssl);
|
||||
waitForServices =
|
||||
{ ... }:
|
||||
[
|
||||
"paperless-web.service"
|
||||
"nginx.service"
|
||||
];
|
||||
waitForPorts =
|
||||
{ ... }:
|
||||
[
|
||||
28981
|
||||
80
|
||||
];
|
||||
waitForUrls = { proto_fqdn, ... }: [ "${proto_fqdn}" ];
|
||||
};
|
||||
|
||||
base =
|
||||
{ config, ... }:
|
||||
{
|
||||
imports = [
|
||||
lib.shb.baseModule
|
||||
../../modules/services/paperless.nix
|
||||
];
|
||||
|
||||
virtualisation.memorySize = 4096;
|
||||
virtualisation.cores = 2;
|
||||
|
||||
test = {
|
||||
inherit subdomain domain;
|
||||
};
|
||||
|
||||
shb.paperless = {
|
||||
enable = true;
|
||||
inherit subdomain domain;
|
||||
};
|
||||
|
||||
# Required for tests
|
||||
environment.systemPackages = [ pkgs.curl ];
|
||||
};
|
||||
|
||||
basic =
|
||||
{ config, ... }:
|
||||
{
|
||||
imports = [ base ];
|
||||
|
||||
test.hasSSL = false;
|
||||
};
|
||||
|
||||
https =
|
||||
{ config, ... }:
|
||||
{
|
||||
imports = [
|
||||
base
|
||||
lib.shb.certs
|
||||
];
|
||||
|
||||
test.hasSSL = true;
|
||||
shb.paperless.ssl = config.shb.certs.certs.selfsigned.n;
|
||||
};
|
||||
|
||||
backup =
|
||||
{ config, ... }:
|
||||
{
|
||||
imports = [
|
||||
https
|
||||
(lib.shb.backup config.shb.paperless.backup)
|
||||
];
|
||||
};
|
||||
|
||||
sso =
|
||||
{ config, ... }:
|
||||
{
|
||||
imports = [
|
||||
https
|
||||
lib.shb.ldap
|
||||
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
|
||||
];
|
||||
|
||||
shb.paperless.sso = {
|
||||
enable = true;
|
||||
provider = "Authelia";
|
||||
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
||||
clientID = "paperless";
|
||||
autoLaunch = true;
|
||||
sharedSecret.result = config.shb.hardcodedsecret.paperlessSSOSecret.result;
|
||||
sharedSecretForAuthelia.result = config.shb.hardcodedsecret.paperlessSSOSecretAuthelia.result;
|
||||
};
|
||||
|
||||
shb.hardcodedsecret.paperlessSSOSecret = {
|
||||
request = config.shb.paperless.sso.sharedSecret.request;
|
||||
settings.content = "paperlessSSOSecret";
|
||||
};
|
||||
|
||||
shb.hardcodedsecret.paperlessSSOSecretAuthelia = {
|
||||
request = config.shb.paperless.sso.sharedSecretForAuthelia.request;
|
||||
settings.content = "paperlessSSOSecret";
|
||||
};
|
||||
|
||||
# Configure LDAP groups for group-based access control
|
||||
shb.lldap.ensureGroups.paperless_user = { };
|
||||
|
||||
shb.lldap.ensureUsers.paperless_test_user = {
|
||||
email = "paperless_user@example.com";
|
||||
groups = [ "paperless_user" ];
|
||||
password.result = config.shb.hardcodedsecret.ldappaperlessUserPassword.result;
|
||||
};
|
||||
|
||||
shb.lldap.ensureUsers.regular_test_user = {
|
||||
email = "regular_user@example.com";
|
||||
groups = [ ];
|
||||
password.result = config.shb.hardcodedsecret.ldapRegularUserPassword.result;
|
||||
};
|
||||
|
||||
shb.hardcodedsecret.ldappaperlessUserPassword = {
|
||||
request = config.shb.lldap.ensureUsers.paperless_test_user.password.request;
|
||||
settings.content = "paperless_user_password";
|
||||
};
|
||||
|
||||
shb.hardcodedsecret.ldapRegularUserPassword = {
|
||||
request = config.shb.lldap.ensureUsers.regular_test_user.password.request;
|
||||
settings.content = "regular_user_password";
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
basic = pkgs.nixosTest {
|
||||
name = "paperless-basic";
|
||||
|
||||
nodes.server = basic;
|
||||
nodes.client = { };
|
||||
|
||||
testScript = commonTestScript;
|
||||
};
|
||||
|
||||
https = pkgs.nixosTest {
|
||||
name = "paperless-https";
|
||||
|
||||
nodes.server = https;
|
||||
nodes.client = { };
|
||||
|
||||
testScript = commonTestScript;
|
||||
};
|
||||
|
||||
sso = pkgs.nixosTest {
|
||||
name = "paperless-https";
|
||||
|
||||
nodes.server = sso;
|
||||
nodes.client = { };
|
||||
|
||||
testScript = commonTestScript;
|
||||
};
|
||||
|
||||
backup = pkgs.nixosTest {
|
||||
name = "paperless-backup";
|
||||
|
||||
nodes.server = backup;
|
||||
nodes.client = { };
|
||||
|
||||
testScript =
|
||||
(lib.shb.mkScripts {
|
||||
hasSSL = args: !(isNull args.node.config.shb.paperless.ssl);
|
||||
waitForServices = args: [
|
||||
"paperless-web.service"
|
||||
"nginx.service"
|
||||
];
|
||||
waitForPorts = args: [
|
||||
28981
|
||||
80
|
||||
];
|
||||
waitForUrls = args: [ "${args.proto_fqdn}" ];
|
||||
}).backup;
|
||||
};
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue