448 lines
12 KiB
Nix
448 lines
12 KiB
Nix
{
|
|
config,
|
|
pkgs,
|
|
lib,
|
|
shb,
|
|
...
|
|
}:
|
|
|
|
let
|
|
cfg = config.shb.paperless;
|
|
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 = 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 = [
|
|
../../lib/module.nix
|
|
../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 shb.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 = shb.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 = shb.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 = shb.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 = shb.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 = shb.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/"
|
|
];
|
|
}
|
|
];
|
|
};
|
|
}
|