style: format tree
This commit is contained in:
@@ -1,4 +1,9 @@
|
|||||||
{ config, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
inherit (lib)
|
inherit (lib)
|
||||||
mkOption
|
mkOption
|
||||||
@@ -7,12 +12,12 @@ let
|
|||||||
auth-passthru = config.selfprivacy.passthru.auth;
|
auth-passthru = config.selfprivacy.passthru.auth;
|
||||||
keys-path = auth-passthru.keys-path;
|
keys-path = auth-passthru.keys-path;
|
||||||
# generate OAuth2 client secret
|
# generate OAuth2 client secret
|
||||||
mkKanidmExecStartPreScript = oauthClientID: linuxGroup:
|
mkKanidmExecStartPreScript =
|
||||||
|
oauthClientID: linuxGroup:
|
||||||
let
|
let
|
||||||
secretFP = auth-passthru.mkOAuth2ClientSecretFP linuxGroup;
|
secretFP = auth-passthru.mkOAuth2ClientSecretFP linuxGroup;
|
||||||
in
|
in
|
||||||
pkgs.writeShellScript
|
pkgs.writeShellScript "${oauthClientID}-kanidm-ExecStartPre-script.sh" ''
|
||||||
"${oauthClientID}-kanidm-ExecStartPre-script.sh" ''
|
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
set -o errexit
|
set -o errexit
|
||||||
if ! [ -f "${secretFP}" ]
|
if ! [ -f "${secretFP}" ]
|
||||||
@@ -22,17 +27,16 @@ let
|
|||||||
chmod 640 "${secretFP}"
|
chmod 640 "${secretFP}"
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
mkKanidmExecStartPostScript = oauthClientID: linuxGroup: isMailserver:
|
mkKanidmExecStartPostScript =
|
||||||
|
oauthClientID: linuxGroup: isMailserver:
|
||||||
let
|
let
|
||||||
kanidmServiceAccountName = "sp.${oauthClientID}.service-account";
|
kanidmServiceAccountName = "sp.${oauthClientID}.service-account";
|
||||||
kanidmServiceAccountTokenName = "${oauthClientID}-service-account-token";
|
kanidmServiceAccountTokenName = "${oauthClientID}-service-account-token";
|
||||||
kanidmServiceAccountTokenFP =
|
kanidmServiceAccountTokenFP = auth-passthru.mkServiceAccountTokenFP linuxGroup;
|
||||||
auth-passthru.mkServiceAccountTokenFP linuxGroup;
|
|
||||||
isRW = oauthClientID == "selfprivacy-api";
|
isRW = oauthClientID == "selfprivacy-api";
|
||||||
in
|
in
|
||||||
pkgs.writeShellScript
|
pkgs.writeShellScript "${oauthClientID}-kanidm-ExecStartPost-script.sh" (
|
||||||
"${oauthClientID}-kanidm-ExecStartPost-script.sh"
|
''
|
||||||
(''
|
|
||||||
export HOME=$RUNTIME_DIRECTORY/client_home
|
export HOME=$RUNTIME_DIRECTORY/client_home
|
||||||
readonly KANIDM="${pkgs.kanidm}/bin/kanidm"
|
readonly KANIDM="${pkgs.kanidm}/bin/kanidm"
|
||||||
|
|
||||||
@@ -82,13 +86,12 @@ let
|
|||||||
+ lib.strings.optionalString isRW ''
|
+ lib.strings.optionalString isRW ''
|
||||||
$KANIDM group add-members idm_admins "${kanidmServiceAccountName}"
|
$KANIDM group add-members idm_admins "${kanidmServiceAccountName}"
|
||||||
''
|
''
|
||||||
);
|
);
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.selfprivacy.auth = {
|
options.selfprivacy.auth = {
|
||||||
clients = mkOption {
|
clients = mkOption {
|
||||||
description =
|
description = "Configurations for OAuth2 & LDAP servers clients services. Corresponding Kanidm provisioning configuration and systemd scripts are generated.";
|
||||||
"Configurations for OAuth2 & LDAP servers clients services. Corresponding Kanidm provisioning configuration and systemd scripts are generated.";
|
|
||||||
default = { };
|
default = { };
|
||||||
type = types.attrsOf (
|
type = types.attrsOf (
|
||||||
types.submodule {
|
types.submodule {
|
||||||
@@ -107,69 +110,58 @@ in
|
|||||||
};
|
};
|
||||||
enablePkce = mkOption {
|
enablePkce = mkOption {
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
description =
|
description = "Whether PKCE must be used between client and Kanidm.";
|
||||||
"Whether PKCE must be used between client and Kanidm.";
|
|
||||||
default = false;
|
default = false;
|
||||||
};
|
};
|
||||||
adminsGroup = mkOption {
|
adminsGroup = mkOption {
|
||||||
type =
|
type = types.nullOr (lib.types.strMatching "sp\.[A-Za-z0-9]+\.admins");
|
||||||
types.nullOr (lib.types.strMatching "sp\.[A-Za-z0-9]+\.admins");
|
description = "Name of admins group in Kanidm, whose members have admin level access to resources (service) associated with OAuth2 client authorization.";
|
||||||
description =
|
|
||||||
"Name of admins group in Kanidm, whose members have admin level access to resources (service) associated with OAuth2 client authorization.";
|
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
usersGroup = mkOption {
|
usersGroup = mkOption {
|
||||||
type =
|
type = types.nullOr (lib.types.strMatching "sp\.[A-Za-z0-9]+\.users");
|
||||||
types.nullOr (lib.types.strMatching "sp\.[A-Za-z0-9]+\.users");
|
description = "Name of users group in Kanidm, whose members have user level access to resources (service) associated with OAuth2 client authorization.";
|
||||||
description =
|
|
||||||
"Name of users group in Kanidm, whose members have user level access to resources (service) associated with OAuth2 client authorization.";
|
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
originLanding = mkOption {
|
originLanding = mkOption {
|
||||||
type = types.nullOr lib.types.str;
|
type = types.nullOr lib.types.str;
|
||||||
description =
|
description = "The origin landing of the service for OAuth2 redirects.";
|
||||||
"The origin landing of the service for OAuth2 redirects.";
|
|
||||||
};
|
};
|
||||||
originUrl = mkOption {
|
originUrl = mkOption {
|
||||||
type = types.nullOr lib.types.str;
|
type = types.nullOr lib.types.str;
|
||||||
description =
|
description = "The origin URL of the service for OAuth2 redirects.";
|
||||||
"The origin URL of the service for OAuth2 redirects.";
|
|
||||||
};
|
};
|
||||||
subdomain = lib.mkOption {
|
subdomain = lib.mkOption {
|
||||||
type =
|
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
||||||
lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
|
||||||
description = "Subdomain of the service.";
|
description = "Subdomain of the service.";
|
||||||
};
|
};
|
||||||
# when true, "name" is passed to a service instead of "name@domain"
|
# when true, "name" is passed to a service instead of "name@domain"
|
||||||
useShortPreferredUsername = mkOption {
|
useShortPreferredUsername = mkOption {
|
||||||
description =
|
description = "Use 'name' instead of 'spn' in the preferred_username claim.";
|
||||||
"Use 'name' instead of 'spn' in the preferred_username claim.";
|
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
};
|
};
|
||||||
linuxUserOfClient = mkOption {
|
linuxUserOfClient = mkOption {
|
||||||
type = types.nullOr lib.types.str;
|
type = types.nullOr lib.types.str;
|
||||||
description =
|
description = "Name of a Linux OAuth2 client user, under which it should get access through a folder with keys.";
|
||||||
"Name of a Linux OAuth2 client user, under which it should get access through a folder with keys.";
|
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
linuxGroupOfClient = mkOption {
|
linuxGroupOfClient = mkOption {
|
||||||
type = types.nullOr lib.types.str;
|
type = types.nullOr lib.types.str;
|
||||||
description =
|
description = "Name of Linux OAuth2 client group, under which it should read an OAuth2 client secret file.";
|
||||||
"Name of Linux OAuth2 client group, under which it should read an OAuth2 client secret file.";
|
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
isTokenNeeded = mkOption {
|
isTokenNeeded = mkOption {
|
||||||
description =
|
description = "Whether a read-only needs to be generated for LDAP access.";
|
||||||
"Whether a read-only needs to be generated for LDAP access.";
|
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = false;
|
default = false;
|
||||||
};
|
};
|
||||||
clientSystemdUnits = mkOption {
|
clientSystemdUnits = mkOption {
|
||||||
description = "A list of systemd services, which depend on OAuth service";
|
description = "A list of systemd services, which depend on OAuth service";
|
||||||
# taken from nixos/lib/systemd-lib.nix: unitNameType
|
# taken from nixos/lib/systemd-lib.nix: unitNameType
|
||||||
type = types.listOf
|
type = types.listOf (
|
||||||
(types.strMatching "[a-zA-Z0-9@%:_.\\-]+[.](service|socket|device|mount|automount|swap|target|path|timer|scope|slice)");
|
types.strMatching "[a-zA-Z0-9@%:_.\\-]+[.](service|socket|device|mount|automount|swap|target|path|timer|scope|slice)"
|
||||||
|
);
|
||||||
};
|
};
|
||||||
scopeMaps = mkOption {
|
scopeMaps = mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
@@ -231,133 +223,132 @@ in
|
|||||||
};
|
};
|
||||||
config = lib.mkIf config.selfprivacy.sso.enable (
|
config = lib.mkIf config.selfprivacy.sso.enable (
|
||||||
let
|
let
|
||||||
clientsAttrsList = lib.attrsets.mapAttrsToList
|
clientsAttrsList = lib.attrsets.mapAttrsToList (
|
||||||
(name: attrs: attrs // rec {
|
name: attrs:
|
||||||
clientID =
|
attrs
|
||||||
if attrs.clientID == null
|
// rec {
|
||||||
then name
|
clientID = if attrs.clientID == null then name else attrs.clientID;
|
||||||
else attrs.clientID;
|
displayName = if attrs.displayName == null then clientID else attrs.displayName;
|
||||||
displayName =
|
adminsGroup = if attrs.adminsGroup == null then "sp.${clientID}.admins" else attrs.adminsGroup;
|
||||||
if attrs.displayName == null
|
usersGroup = if attrs.usersGroup == null then "sp.${clientID}.users" else attrs.usersGroup;
|
||||||
then clientID
|
basicSecretFile = "${keys-path}/${linuxGroupOfClient}/kanidm-oauth-client-secret";
|
||||||
else attrs.displayName;
|
linuxUserOfClient = if attrs.linuxUserOfClient == null then clientID else attrs.linuxUserOfClient;
|
||||||
adminsGroup =
|
|
||||||
if attrs.adminsGroup == null
|
|
||||||
then "sp.${clientID}.admins"
|
|
||||||
else attrs.adminsGroup;
|
|
||||||
usersGroup =
|
|
||||||
if attrs.usersGroup == null
|
|
||||||
then "sp.${clientID}.users"
|
|
||||||
else attrs.usersGroup;
|
|
||||||
basicSecretFile =
|
|
||||||
"${keys-path}/${linuxGroupOfClient}/kanidm-oauth-client-secret";
|
|
||||||
linuxUserOfClient =
|
|
||||||
if attrs.linuxUserOfClient == null
|
|
||||||
then clientID
|
|
||||||
else attrs.linuxUserOfClient;
|
|
||||||
linuxGroupOfClient =
|
linuxGroupOfClient =
|
||||||
if attrs.linuxGroupOfClient == null
|
if attrs.linuxGroupOfClient == null then clientID else attrs.linuxGroupOfClient;
|
||||||
then clientID
|
|
||||||
else attrs.linuxGroupOfClient;
|
|
||||||
originLanding =
|
originLanding =
|
||||||
if attrs.originLanding == null
|
if attrs.originLanding == null then
|
||||||
then "https://${attrs.subdomain}.${config.selfprivacy.domain}/"
|
"https://${attrs.subdomain}.${config.selfprivacy.domain}/"
|
||||||
else attrs.originLanding;
|
else
|
||||||
|
attrs.originLanding;
|
||||||
scopeMaps =
|
scopeMaps =
|
||||||
if attrs.scopeMaps == null
|
if attrs.scopeMaps == null then
|
||||||
then { "${usersGroup}" = [ "email" "openid" "profile" ]; }
|
{
|
||||||
else attrs.scopeMaps;
|
"${usersGroup}" = [
|
||||||
})
|
"email"
|
||||||
config.selfprivacy.auth.clients;
|
"openid"
|
||||||
|
"profile"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
attrs.scopeMaps;
|
||||||
|
}
|
||||||
|
) config.selfprivacy.auth.clients;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# for each OAuth2 client: member of the `keys` group for directory access
|
# for each OAuth2 client: member of the `keys` group for directory access
|
||||||
users.groups.keys.members = lib.mkMerge (lib.forEach
|
users.groups.keys.members = lib.mkMerge (
|
||||||
clientsAttrsList
|
lib.forEach clientsAttrsList ({ linuxUserOfClient, ... }: [ linuxUserOfClient ])
|
||||||
({ linuxUserOfClient, ... }: [ linuxUserOfClient ])
|
|
||||||
);
|
);
|
||||||
|
|
||||||
systemd.tmpfiles.settings."kanidm-secrets" = lib.mkMerge (lib.forEach
|
systemd.tmpfiles.settings."kanidm-secrets" = lib.mkMerge (
|
||||||
clientsAttrsList
|
lib.forEach clientsAttrsList (
|
||||||
({ linuxGroupOfClient, ... }: {
|
{ linuxGroupOfClient, ... }:
|
||||||
"${keys-path}/${linuxGroupOfClient}".d = {
|
{
|
||||||
user = "kanidm";
|
"${keys-path}/${linuxGroupOfClient}".d = {
|
||||||
group = linuxGroupOfClient;
|
user = "kanidm";
|
||||||
mode = "2750";
|
group = linuxGroupOfClient;
|
||||||
};
|
mode = "2750";
|
||||||
})
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
# for each OAuth2 client: scripts with Kanidm CLI commands
|
# for each OAuth2 client: scripts with Kanidm CLI commands
|
||||||
systemd.services.kanidm = {
|
systemd.services.kanidm = {
|
||||||
before =
|
before = lib.lists.concatMap ({ clientSystemdUnits, ... }: clientSystemdUnits) clientsAttrsList;
|
||||||
lib.lists.concatMap
|
serviceConfig = lib.mkMerge (
|
||||||
({ clientSystemdUnits, ... }: clientSystemdUnits)
|
lib.forEach clientsAttrsList (
|
||||||
clientsAttrsList;
|
{
|
||||||
serviceConfig =
|
clientID,
|
||||||
lib.mkMerge (lib.forEach
|
isTokenNeeded,
|
||||||
clientsAttrsList
|
linuxGroupOfClient,
|
||||||
({ clientID, isTokenNeeded, linuxGroupOfClient, isMailserver, ... }:
|
isMailserver,
|
||||||
{
|
...
|
||||||
ExecStartPre = [
|
}:
|
||||||
# "-" prefix means to ignore exit code of prefixed script
|
{
|
||||||
("-" + mkKanidmExecStartPreScript clientID linuxGroupOfClient)
|
ExecStartPre = [
|
||||||
];
|
# "-" prefix means to ignore exit code of prefixed script
|
||||||
ExecStartPost = lib.mkIf isTokenNeeded
|
("-" + mkKanidmExecStartPreScript clientID linuxGroupOfClient)
|
||||||
(lib.mkAfter [
|
];
|
||||||
("-" +
|
ExecStartPost = lib.mkIf isTokenNeeded (
|
||||||
mkKanidmExecStartPostScript
|
lib.mkAfter [
|
||||||
clientID
|
("-" + mkKanidmExecStartPostScript clientID linuxGroupOfClient isMailserver)
|
||||||
linuxGroupOfClient
|
]
|
||||||
isMailserver)
|
);
|
||||||
]);
|
}
|
||||||
}));
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
# for each OAuth2 client: Kanidm provisioning options
|
# for each OAuth2 client: Kanidm provisioning options
|
||||||
services.kanidm.provision = lib.mkMerge (lib.forEach
|
services.kanidm.provision = lib.mkMerge (
|
||||||
clientsAttrsList
|
lib.forEach clientsAttrsList (
|
||||||
({ adminsGroup
|
{
|
||||||
, basicSecretFile
|
adminsGroup,
|
||||||
, claimMaps
|
basicSecretFile,
|
||||||
, clientID
|
claimMaps,
|
||||||
, displayName
|
clientID,
|
||||||
, enablePkce
|
displayName,
|
||||||
, imageFile
|
enablePkce,
|
||||||
, originLanding
|
imageFile,
|
||||||
, originUrl
|
originLanding,
|
||||||
, scopeMaps
|
originUrl,
|
||||||
, useShortPreferredUsername
|
scopeMaps,
|
||||||
, usersGroup
|
useShortPreferredUsername,
|
||||||
, ...
|
usersGroup,
|
||||||
}: {
|
...
|
||||||
groups = lib.mkIf (clientID != "selfprivacy-api") {
|
}:
|
||||||
"${adminsGroup}".members =
|
{
|
||||||
[ auth-passthru.admins-group ];
|
groups = lib.mkIf (clientID != "selfprivacy-api") {
|
||||||
"${usersGroup}".members =
|
"${adminsGroup}".members = [ auth-passthru.admins-group ];
|
||||||
[ adminsGroup auth-passthru.full-users-group ];
|
"${usersGroup}".members = [
|
||||||
};
|
adminsGroup
|
||||||
systems.oauth2.${clientID} = {
|
auth-passthru.full-users-group
|
||||||
inherit
|
];
|
||||||
basicSecretFile
|
};
|
||||||
claimMaps
|
systems.oauth2.${clientID} = {
|
||||||
displayName
|
inherit
|
||||||
imageFile
|
basicSecretFile
|
||||||
originLanding
|
claimMaps
|
||||||
originUrl
|
displayName
|
||||||
scopeMaps
|
imageFile
|
||||||
;
|
originLanding
|
||||||
preferShortUsername = useShortPreferredUsername;
|
originUrl
|
||||||
allowInsecureClientDisablePkce = ! enablePkce;
|
scopeMaps
|
||||||
removeOrphanedClaimMaps = true;
|
;
|
||||||
|
preferShortUsername = useShortPreferredUsername;
|
||||||
|
allowInsecureClientDisablePkce = !enablePkce;
|
||||||
|
removeOrphanedClaimMaps = true;
|
||||||
|
|
||||||
# NOTE https://github.com/oddlama/kanidm-provision/issues/15
|
# NOTE https://github.com/oddlama/kanidm-provision/issues/15
|
||||||
# add more scopes when a user is a member of specific group
|
# add more scopes when a user is a member of specific group
|
||||||
# currently not possible due to https://github.com/kanidm/kanidm/issues/2882#issuecomment-2564490144
|
# currently not possible due to https://github.com/kanidm/kanidm/issues/2882#issuecomment-2564490144
|
||||||
# supplementaryScopeMaps."${admins-group}" =
|
# supplementaryScopeMaps."${admins-group}" =
|
||||||
# [ "read:admin" "write:admin" ];
|
# [ "read:admin" "write:admin" ];
|
||||||
};
|
};
|
||||||
}));
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,9 @@
|
|||||||
{ config, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
domain = config.selfprivacy.domain;
|
domain = config.selfprivacy.domain;
|
||||||
subdomain = "auth";
|
subdomain = "auth";
|
||||||
@@ -38,7 +43,6 @@ lib.mkIf config.selfprivacy.sso.enable {
|
|||||||
"127.0.0.1" = [ auth-fqdn ];
|
"127.0.0.1" = [ auth-fqdn ];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
# kanidm uses TLS in internal connection with nginx too
|
# kanidm uses TLS in internal connection with nginx too
|
||||||
# FIXME revise this: maybe kanidm must not have access to a public TLS
|
# FIXME revise this: maybe kanidm must not have access to a public TLS
|
||||||
users.groups."acmereceivers".members = [ "kanidm" ];
|
users.groups."acmereceivers".members = [ "kanidm" ];
|
||||||
@@ -69,16 +73,13 @@ lib.mkIf config.selfprivacy.sso.enable {
|
|||||||
origin = "https://" + auth-fqdn;
|
origin = "https://" + auth-fqdn;
|
||||||
|
|
||||||
# TODO revise this: maybe kanidm must not have access to a public TLS
|
# TODO revise this: maybe kanidm must not have access to a public TLS
|
||||||
tls_chain =
|
tls_chain = "${config.security.acme.certs.${domain}.directory}/fullchain.pem";
|
||||||
"${config.security.acme.certs.${domain}.directory}/fullchain.pem";
|
tls_key = "${config.security.acme.certs.${domain}.directory}/key.pem";
|
||||||
tls_key =
|
|
||||||
"${config.security.acme.certs.${domain}.directory}/key.pem";
|
|
||||||
|
|
||||||
# nginx should proxy requests to it
|
# nginx should proxy requests to it
|
||||||
bindaddress = kanidm-bind-address;
|
bindaddress = kanidm-bind-address;
|
||||||
|
|
||||||
ldapbindaddress =
|
ldapbindaddress = "${ldap-host}:${toString ldap-port}";
|
||||||
"${ldap-host}:${toString ldap-port}";
|
|
||||||
|
|
||||||
# kanidm is behind a proxy
|
# kanidm is behind a proxy
|
||||||
trust_x_forward_for = true;
|
trust_x_forward_for = true;
|
||||||
@@ -101,8 +102,7 @@ lib.mkIf config.selfprivacy.sso.enable {
|
|||||||
|
|
||||||
services.nginx = {
|
services.nginx = {
|
||||||
enable = true;
|
enable = true;
|
||||||
additionalModules =
|
additionalModules = lib.mkIf config.selfprivacy.sso.debug [ pkgs.nginxModules.lua ];
|
||||||
lib.mkIf config.selfprivacy.sso.debug [ pkgs.nginxModules.lua ];
|
|
||||||
commonHttpConfig = lib.mkIf config.selfprivacy.sso.debug ''
|
commonHttpConfig = lib.mkIf config.selfprivacy.sso.debug ''
|
||||||
log_format kanidm escape=none '$request $status\n'
|
log_format kanidm escape=none '$request $status\n'
|
||||||
'[Request body]: $request_body\n'
|
'[Request body]: $request_body\n'
|
||||||
@@ -158,8 +158,7 @@ lib.mkIf config.selfprivacy.sso.enable {
|
|||||||
|
|
||||||
systemd.services.kanidm.serviceConfig.ExecStartPre =
|
systemd.services.kanidm.serviceConfig.ExecStartPre =
|
||||||
# idempotent script to run on each startup only for kanidm v1.5.0
|
# idempotent script to run on each startup only for kanidm v1.5.0
|
||||||
lib.mkIf (pkgs.kanidm.version == "1.5.0")
|
lib.mkIf (pkgs.kanidm.version == "1.5.0") (lib.mkBefore [ kanidmMigrateDbScript ]);
|
||||||
(lib.mkBefore [ kanidmMigrateDbScript ]);
|
|
||||||
|
|
||||||
selfprivacy.passthru.auth = {
|
selfprivacy.passthru.auth = {
|
||||||
inherit
|
inherit
|
||||||
@@ -171,25 +170,20 @@ lib.mkIf config.selfprivacy.sso.enable {
|
|||||||
keys-path
|
keys-path
|
||||||
;
|
;
|
||||||
oauth2-introspection-url-prefix = client_id: "https://${client_id}:";
|
oauth2-introspection-url-prefix = client_id: "https://${client_id}:";
|
||||||
oauth2-introspection-url-postfix =
|
oauth2-introspection-url-postfix = "@${auth-fqdn}/oauth2/token/introspect";
|
||||||
"@${auth-fqdn}/oauth2/token/introspect";
|
oauth2-discovery-url =
|
||||||
oauth2-discovery-url = client_id:
|
client_id: "https://${auth-fqdn}/oauth2/openid/${client_id}/.well-known/openid-configuration";
|
||||||
"https://${auth-fqdn}/oauth2/openid/${client_id}/.well-known/openid-configuration";
|
|
||||||
oauth2-provider-name = "Kanidm";
|
oauth2-provider-name = "Kanidm";
|
||||||
oauth2-systemd-service = "kanidm.service";
|
oauth2-systemd-service = "kanidm.service";
|
||||||
|
|
||||||
# e.g. "dc=mydomain,dc=com"
|
# e.g. "dc=mydomain,dc=com"
|
||||||
ldap-base-dn =
|
ldap-base-dn = lib.strings.concatMapStringsSep "," (x: "dc=" + x) (
|
||||||
lib.strings.concatMapStringsSep
|
lib.strings.splitString "." domain
|
||||||
","
|
);
|
||||||
(x: "dc=" + x)
|
|
||||||
(lib.strings.splitString "." domain);
|
|
||||||
|
|
||||||
# TODO consider to pass a value or throw exception if token is not generated
|
# TODO consider to pass a value or throw exception if token is not generated
|
||||||
mkServiceAccountTokenFP = linuxGroup:
|
mkServiceAccountTokenFP = linuxGroup: "${keys-path}/${linuxGroup}/kanidm-service-account-token";
|
||||||
"${keys-path}/${linuxGroup}/kanidm-service-account-token";
|
|
||||||
|
|
||||||
mkOAuth2ClientSecretFP = linuxGroup:
|
mkOAuth2ClientSecretFP = linuxGroup: "${keys-path}/${linuxGroup}/kanidm-oauth-client-secret";
|
||||||
"${keys-path}/${linuxGroup}/kanidm-oauth-client-secret";
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
177
auth/kanidm.nix
177
auth/kanidm.nix
@@ -1,8 +1,9 @@
|
|||||||
{ config
|
{
|
||||||
, lib
|
config,
|
||||||
, options
|
lib,
|
||||||
, pkgs
|
options,
|
||||||
, ...
|
pkgs,
|
||||||
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
inherit (lib)
|
inherit (lib)
|
||||||
@@ -40,7 +41,9 @@ let
|
|||||||
cfg = config.services.kanidm;
|
cfg = config.services.kanidm;
|
||||||
settingsFormat = pkgs.formats.toml { };
|
settingsFormat = pkgs.formats.toml { };
|
||||||
# Remove null values, so we can document optional values that don't end up in the generated TOML file.
|
# Remove null values, so we can document optional values that don't end up in the generated TOML file.
|
||||||
filterConfig = converge (a: filterAttrsRecursive (_: v: v != null) (builtins.removeAttrs a [ "provision" ]));
|
filterConfig = converge (
|
||||||
|
a: filterAttrsRecursive (_: v: v != null) (builtins.removeAttrs a [ "provision" ])
|
||||||
|
);
|
||||||
serverConfigFile = settingsFormat.generate "server.toml" (filterConfig cfg.serverSettings);
|
serverConfigFile = settingsFormat.generate "server.toml" (filterConfig cfg.serverSettings);
|
||||||
clientConfigFile = settingsFormat.generate "kanidm-config.toml" (filterConfig cfg.clientSettings);
|
clientConfigFile = settingsFormat.generate "kanidm-config.toml" (filterConfig cfg.clientSettings);
|
||||||
unixConfigFile = settingsFormat.generate "kanidm-unixd.toml" (filterConfig cfg.unixSettings);
|
unixConfigFile = settingsFormat.generate "kanidm-unixd.toml" (filterConfig cfg.unixSettings);
|
||||||
@@ -54,17 +57,16 @@ let
|
|||||||
# paths, no new bind mount is added. Adding subpaths caused problems on ofborg.
|
# paths, no new bind mount is added. Adding subpaths caused problems on ofborg.
|
||||||
hasPrefixInList =
|
hasPrefixInList =
|
||||||
list: newPath: any (path: hasPrefix (builtins.toString path) (builtins.toString newPath)) list;
|
list: newPath: any (path: hasPrefix (builtins.toString path) (builtins.toString newPath)) list;
|
||||||
mergePaths = foldl'
|
mergePaths = foldl' (
|
||||||
(
|
merged: newPath:
|
||||||
merged: newPath:
|
let
|
||||||
let
|
# If the new path is a prefix to some existing path, we need to filter it out
|
||||||
# If the new path is a prefix to some existing path, we need to filter it out
|
filteredPaths = filter (p: !hasPrefix (builtins.toString newPath) (builtins.toString p)) merged;
|
||||||
filteredPaths = filter (p: !hasPrefix (builtins.toString newPath) (builtins.toString p)) merged;
|
# If a prefix of the new path is already in the list, do not add it
|
||||||
# If a prefix of the new path is already in the list, do not add it
|
filteredNew = optional (!hasPrefixInList filteredPaths newPath) newPath;
|
||||||
filteredNew = optional (!hasPrefixInList filteredPaths newPath) newPath;
|
in
|
||||||
in
|
filteredPaths ++ filteredNew
|
||||||
filteredPaths ++ filteredNew
|
) [ ];
|
||||||
) [ ];
|
|
||||||
|
|
||||||
defaultServiceConfig = {
|
defaultServiceConfig = {
|
||||||
# Setting the type to notify enables additional healthchecks, ensuring units
|
# Setting the type to notify enables additional healthchecks, ensuring units
|
||||||
@@ -127,19 +129,20 @@ let
|
|||||||
filterPresent = filterAttrs (_: v: v.present);
|
filterPresent = filterAttrs (_: v: v.present);
|
||||||
|
|
||||||
selfprivacy-admin-groups-regex = "^sp\.([[:alnum:]]+\.|)admins$";
|
selfprivacy-admin-groups-regex = "^sp\.([[:alnum:]]+\.|)admins$";
|
||||||
is-selfprivacy-admin-group = name:
|
is-selfprivacy-admin-group =
|
||||||
! builtins.isNull (builtins.match selfprivacy-admin-groups-regex name);
|
name: !builtins.isNull (builtins.match selfprivacy-admin-groups-regex name);
|
||||||
|
|
||||||
isGroupNonOverwritable = g: false
|
isGroupNonOverwritable =
|
||||||
|| ! g ? members
|
g:
|
||||||
|
false
|
||||||
|
|| !g ? members
|
||||||
|| g ? members && g.members == [ ]
|
|| g ? members && g.members == [ ]
|
||||||
|| g ? members && builtins.any is-selfprivacy-admin-group g.members;
|
|| g ? members && builtins.any is-selfprivacy-admin-group g.members;
|
||||||
|
|
||||||
provisionStateJson = pkgs.writeText "provision-state.json" (
|
provisionStateJson = pkgs.writeText "provision-state.json" (
|
||||||
builtins.toJSON {
|
builtins.toJSON {
|
||||||
inherit (cfg.provision) persons systems;
|
inherit (cfg.provision) persons systems;
|
||||||
groups =
|
groups = lib.attrsets.filterAttrs (_n: v: !isGroupNonOverwritable v) cfg.provision.groups;
|
||||||
lib.attrsets.filterAttrs (_n: v: ! isGroupNonOverwritable v) cfg.provision.groups;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -182,8 +185,9 @@ let
|
|||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
|
|
||||||
groupsToCreateAndPopulate =
|
groupsToCreateAndPopulate = lib.attrsets.filterAttrs (
|
||||||
lib.attrsets.filterAttrs (_n: isGroupNonOverwritable) cfg.provision.groups;
|
_n: isGroupNonOverwritable
|
||||||
|
) cfg.provision.groups;
|
||||||
|
|
||||||
createGroups = ''
|
createGroups = ''
|
||||||
for group_name in ${lib.strings.concatStringsSep " " (builtins.attrNames groupsToCreateAndPopulate)}
|
for group_name in ${lib.strings.concatStringsSep " " (builtins.attrNames groupsToCreateAndPopulate)}
|
||||||
@@ -199,9 +203,9 @@ let
|
|||||||
done
|
done
|
||||||
'';
|
'';
|
||||||
|
|
||||||
createAndPopulateGroups =
|
createAndPopulateGroups = lib.concatLines (
|
||||||
lib.concatLines ([ createGroups ]
|
[ createGroups ] ++ (lib.mapAttrsToList populateGroup groupsToCreateAndPopulate)
|
||||||
++ (lib.mapAttrsToList populateGroup groupsToCreateAndPopulate));
|
);
|
||||||
|
|
||||||
postStartScript = pkgs.writeShellScript "post-start" ''
|
postStartScript = pkgs.writeShellScript "post-start" ''
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -250,11 +254,11 @@ let
|
|||||||
last (splitString "]:" cfg.serverSettings.bindaddress)
|
last (splitString "]:" cfg.serverSettings.bindaddress)
|
||||||
else
|
else
|
||||||
# ipv4:
|
# ipv4:
|
||||||
if hasInfix "." cfg.serverSettings.bindaddress then
|
if hasInfix "." cfg.serverSettings.bindaddress then
|
||||||
last (splitString ":" cfg.serverSettings.bindaddress)
|
last (splitString ":" cfg.serverSettings.bindaddress)
|
||||||
# default is 8443
|
# default is 8443
|
||||||
else
|
else
|
||||||
"8443";
|
"8443";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.services.kanidm = {
|
options.services.kanidm = {
|
||||||
@@ -476,11 +480,9 @@ in
|
|||||||
config.members = concatLists (
|
config.members = concatLists (
|
||||||
flip mapAttrsToList cfg.provision.persons (
|
flip mapAttrsToList cfg.provision.persons (
|
||||||
person: personCfg:
|
person: personCfg:
|
||||||
optional
|
optional (
|
||||||
(
|
personCfg.present && builtins.elem groupSubmod.config._module.args.name personCfg.groups
|
||||||
personCfg.present && builtins.elem groupSubmod.config._module.args.name personCfg.groups
|
) person
|
||||||
)
|
|
||||||
person
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -683,12 +685,9 @@ in
|
|||||||
++ entityList "oauth2" cfg.provision.systems.oauth2;
|
++ entityList "oauth2" cfg.provision.systems.oauth2;
|
||||||
|
|
||||||
# Accumulate entities by name. Track corresponding entity types for later duplicate check.
|
# Accumulate entities by name. Track corresponding entity types for later duplicate check.
|
||||||
entitiesByName = foldl'
|
entitiesByName = foldl' (
|
||||||
(
|
acc: { type, name }: acc // { ${name} = (acc.${name} or [ ]) ++ [ type ]; }
|
||||||
acc: { type, name }: acc // { ${name} = (acc.${name} or [ ]) ++ [ type ]; }
|
) { } entities;
|
||||||
)
|
|
||||||
{ }
|
|
||||||
entities;
|
|
||||||
|
|
||||||
assertGroupsKnown =
|
assertGroupsKnown =
|
||||||
opt: groups:
|
opt: groups:
|
||||||
@@ -800,59 +799,59 @@ in
|
|||||||
]
|
]
|
||||||
++ flip mapAttrsToList (filterPresent cfg.provision.persons) (
|
++ flip mapAttrsToList (filterPresent cfg.provision.persons) (
|
||||||
person: personCfg:
|
person: personCfg:
|
||||||
assertGroupsKnown "services.kanidm.provision.persons.${person}.groups" personCfg.groups
|
assertGroupsKnown "services.kanidm.provision.persons.${person}.groups" personCfg.groups
|
||||||
)
|
)
|
||||||
++ flip mapAttrsToList (filterPresent cfg.provision.groups) (
|
++ flip mapAttrsToList (filterPresent cfg.provision.groups) (
|
||||||
group: groupCfg:
|
group: groupCfg:
|
||||||
assertEntitiesKnown "services.kanidm.provision.groups.${group}.members" groupCfg.members
|
assertEntitiesKnown "services.kanidm.provision.groups.${group}.members" groupCfg.members
|
||||||
)
|
)
|
||||||
++ concatLists (
|
++ concatLists (
|
||||||
flip mapAttrsToList (filterPresent cfg.provision.systems.oauth2) (
|
flip mapAttrsToList (filterPresent cfg.provision.systems.oauth2) (
|
||||||
oauth2: oauth2Cfg:
|
oauth2: oauth2Cfg:
|
||||||
[
|
[
|
||||||
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.scopeMaps" (
|
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.scopeMaps" (
|
||||||
attrNames oauth2Cfg.scopeMaps
|
attrNames oauth2Cfg.scopeMaps
|
||||||
))
|
))
|
||||||
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.supplementaryScopeMaps" (
|
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.supplementaryScopeMaps" (
|
||||||
attrNames oauth2Cfg.supplementaryScopeMaps
|
attrNames oauth2Cfg.supplementaryScopeMaps
|
||||||
))
|
))
|
||||||
]
|
]
|
||||||
++ concatLists (
|
++ concatLists (
|
||||||
flip mapAttrsToList oauth2Cfg.claimMaps (
|
flip mapAttrsToList oauth2Cfg.claimMaps (
|
||||||
claim: claimCfg: [
|
claim: claimCfg: [
|
||||||
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim}.valuesByGroup" (
|
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim}.valuesByGroup" (
|
||||||
attrNames claimCfg.valuesByGroup
|
attrNames claimCfg.valuesByGroup
|
||||||
))
|
))
|
||||||
# At least one group must map to a value in each claim map
|
# At least one group must map to a value in each claim map
|
||||||
{
|
{
|
||||||
assertion =
|
assertion =
|
||||||
(cfg.provision.enable && cfg.enableServer)
|
(cfg.provision.enable && cfg.enableServer)
|
||||||
-> any (xs: xs != [ ]) (attrValues claimCfg.valuesByGroup);
|
-> any (xs: xs != [ ]) (attrValues claimCfg.valuesByGroup);
|
||||||
message = "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim} does not specify any values for any group";
|
message = "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim} does not specify any values for any group";
|
||||||
}
|
}
|
||||||
# Public clients cannot define a basic secret
|
# Public clients cannot define a basic secret
|
||||||
{
|
{
|
||||||
assertion =
|
assertion =
|
||||||
(cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) -> oauth2Cfg.basicSecretFile == null;
|
(cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) -> oauth2Cfg.basicSecretFile == null;
|
||||||
message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot specify a basic secret";
|
message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot specify a basic secret";
|
||||||
}
|
}
|
||||||
# Public clients cannot disable PKCE
|
# Public clients cannot disable PKCE
|
||||||
{
|
{
|
||||||
assertion =
|
assertion =
|
||||||
(cfg.provision.enable && cfg.enableServer && oauth2Cfg.public)
|
(cfg.provision.enable && cfg.enableServer && oauth2Cfg.public)
|
||||||
-> !oauth2Cfg.allowInsecureClientDisablePkce;
|
-> !oauth2Cfg.allowInsecureClientDisablePkce;
|
||||||
message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot disable PKCE";
|
message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot disable PKCE";
|
||||||
}
|
}
|
||||||
# Non-public clients cannot enable localhost redirects
|
# Non-public clients cannot enable localhost redirects
|
||||||
{
|
{
|
||||||
assertion =
|
assertion =
|
||||||
(cfg.provision.enable && cfg.enableServer && !oauth2Cfg.public)
|
(cfg.provision.enable && cfg.enableServer && !oauth2Cfg.public)
|
||||||
-> !oauth2Cfg.enableLocalhostRedirects;
|
-> !oauth2Cfg.enableLocalhostRedirects;
|
||||||
message = "services.kanidm.provision.systems.oauth2.${oauth2} is a non-public client and thus cannot enable localhost redirects";
|
message = "services.kanidm.provision.systems.oauth2.${oauth2} is a non-public client and thus cannot enable localhost redirects";
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@@ -1,4 +1,9 @@
|
|||||||
{ config, pkgs, lib, ... }:
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
redis-sp-api-srv-name = "sp-api";
|
redis-sp-api-srv-name = "sp-api";
|
||||||
sp-print-api-token = pkgs.writeShellApplication {
|
sp-print-api-token = pkgs.writeShellApplication {
|
||||||
@@ -76,7 +81,8 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
services.do-agent.enable = if config.selfprivacy.server.provider == "DIGITALOCEAN" then true else false;
|
services.do-agent.enable =
|
||||||
|
if config.selfprivacy.server.provider == "DIGITALOCEAN" then true else false;
|
||||||
|
|
||||||
boot.tmp.cleanOnBoot = true;
|
boot.tmp.cleanOnBoot = true;
|
||||||
networking = {
|
networking = {
|
||||||
@@ -84,14 +90,31 @@ in
|
|||||||
domain = config.selfprivacy.domain;
|
domain = config.selfprivacy.domain;
|
||||||
usePredictableInterfaceNames = false;
|
usePredictableInterfaceNames = false;
|
||||||
firewall = {
|
firewall = {
|
||||||
allowedTCPPorts = [ 22 25 80 143 443 465 587 993 4443 8443 ];
|
allowedTCPPorts = [
|
||||||
allowedUDPPorts = [ 8443 10000 ];
|
22
|
||||||
|
25
|
||||||
|
80
|
||||||
|
143
|
||||||
|
443
|
||||||
|
465
|
||||||
|
587
|
||||||
|
993
|
||||||
|
4443
|
||||||
|
8443
|
||||||
|
];
|
||||||
|
allowedUDPPorts = [
|
||||||
|
8443
|
||||||
|
10000
|
||||||
|
];
|
||||||
extraCommands = ''
|
extraCommands = ''
|
||||||
iptables --table nat --append POSTROUTING --out-interface eth0 -j MASQUERADE
|
iptables --table nat --append POSTROUTING --out-interface eth0 -j MASQUERADE
|
||||||
iptables --append FORWARD --in-interface vpn00 -j ACCEPT
|
iptables --append FORWARD --in-interface vpn00 -j ACCEPT
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
nameservers = [ "1.1.1.1" "1.0.0.1" ];
|
nameservers = [
|
||||||
|
"1.1.1.1"
|
||||||
|
"1.0.0.1"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
time.timeZone = config.selfprivacy.timezone;
|
time.timeZone = config.selfprivacy.timezone;
|
||||||
i18n.defaultLocale = "en_GB.UTF-8";
|
i18n.defaultLocale = "en_GB.UTF-8";
|
||||||
@@ -107,8 +130,15 @@ in
|
|||||||
};
|
};
|
||||||
services.fail2ban.enable = true;
|
services.fail2ban.enable = true;
|
||||||
programs.ssh = {
|
programs.ssh = {
|
||||||
pubkeyAcceptedKeyTypes = [ "ssh-ed25519" "ssh-rsa" "ecdsa-sha2-nistp256" ];
|
pubkeyAcceptedKeyTypes = [
|
||||||
hostKeyAlgorithms = [ "ssh-ed25519" "ssh-rsa" ];
|
"ssh-ed25519"
|
||||||
|
"ssh-rsa"
|
||||||
|
"ecdsa-sha2-nistp256"
|
||||||
|
];
|
||||||
|
hostKeyAlgorithms = [
|
||||||
|
"ssh-ed25519"
|
||||||
|
"ssh-rsa"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
git
|
git
|
||||||
@@ -124,9 +154,9 @@ in
|
|||||||
"R! /old-root"
|
"R! /old-root"
|
||||||
"d /etc/selfprivacy/dump 0700 0700 selfprivacy-api selfprivacy-api"
|
"d /etc/selfprivacy/dump 0700 0700 selfprivacy-api selfprivacy-api"
|
||||||
];
|
];
|
||||||
system.stateVersion =
|
system.stateVersion = lib.mkIf (
|
||||||
lib.mkIf (config.selfprivacy.stateVersion != null)
|
config.selfprivacy.stateVersion != null
|
||||||
config.selfprivacy.stateVersion;
|
) config.selfprivacy.stateVersion;
|
||||||
system.autoUpgrade = {
|
system.autoUpgrade = {
|
||||||
enable = config.selfprivacy.autoUpgrade.enable;
|
enable = config.selfprivacy.autoUpgrade.enable;
|
||||||
allowReboot = config.selfprivacy.autoUpgrade.allowReboot;
|
allowReboot = config.selfprivacy.autoUpgrade.allowReboot;
|
||||||
@@ -168,7 +198,11 @@ in
|
|||||||
};
|
};
|
||||||
nix.settings = {
|
nix.settings = {
|
||||||
sandbox = true;
|
sandbox = true;
|
||||||
experimental-features = [ "nix-command" "flakes" "repl-flake" ];
|
experimental-features = [
|
||||||
|
"nix-command"
|
||||||
|
"flakes"
|
||||||
|
"repl-flake"
|
||||||
|
];
|
||||||
# auto-optimise-store = true;
|
# auto-optimise-store = true;
|
||||||
|
|
||||||
# evaluation restrictions:
|
# evaluation restrictions:
|
||||||
|
258
flake.nix
258
flake.nix
@@ -2,148 +2,156 @@
|
|||||||
description = "SelfPrivacy NixOS configuration flake";
|
description = "SelfPrivacy NixOS configuration flake";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = github:nixos/nixpkgs;
|
nixpkgs.url = "github:nixos/nixpkgs";
|
||||||
nixos-unstable.url = github:nixos/nixpkgs/nixos-unstable;
|
nixos-unstable.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
|
|
||||||
selfprivacy-api.url =
|
selfprivacy-api.url = "git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git";
|
||||||
git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git;
|
|
||||||
# make selfprivacy-api use the same shared nixpkgs
|
# make selfprivacy-api use the same shared nixpkgs
|
||||||
selfprivacy-api.inputs.nixpkgs.follows = "nixpkgs";
|
selfprivacy-api.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, nixos-unstable, selfprivacy-api }: {
|
outputs =
|
||||||
nixosConfigurations-fun =
|
{
|
||||||
{ hardware-configuration
|
self,
|
||||||
, deployment
|
nixpkgs,
|
||||||
, userdata
|
nixos-unstable,
|
||||||
, top-level-flake
|
selfprivacy-api,
|
||||||
, sp-modules
|
}:
|
||||||
}:
|
{
|
||||||
{
|
nixosConfigurations-fun =
|
||||||
default = nixpkgs.lib.nixosSystem {
|
{
|
||||||
modules = [
|
hardware-configuration,
|
||||||
hardware-configuration
|
deployment,
|
||||||
deployment
|
userdata,
|
||||||
./configuration.nix
|
top-level-flake,
|
||||||
./auth/auth.nix
|
sp-modules,
|
||||||
{
|
}:
|
||||||
nixpkgs.overlays = [
|
{
|
||||||
(
|
default = nixpkgs.lib.nixosSystem {
|
||||||
_final: prev:
|
modules =
|
||||||
{
|
[
|
||||||
|
hardware-configuration
|
||||||
|
deployment
|
||||||
|
./configuration.nix
|
||||||
|
./auth/auth.nix
|
||||||
|
{
|
||||||
|
nixpkgs.overlays = [
|
||||||
|
(_final: prev: {
|
||||||
inherit (nixos-unstable.legacyPackages.${prev.system})
|
inherit (nixos-unstable.legacyPackages.${prev.system})
|
||||||
kanidm
|
kanidm
|
||||||
kanidm-provision
|
kanidm-provision
|
||||||
;
|
;
|
||||||
}
|
})
|
||||||
|
];
|
||||||
|
disabledModules = [ "services/security/kanidm.nix" ];
|
||||||
|
imports = [ ./auth/kanidm.nix ];
|
||||||
|
}
|
||||||
|
selfprivacy-api.nixosModules.default
|
||||||
|
(
|
||||||
|
{ pkgs, lib, ... }:
|
||||||
|
{
|
||||||
|
environment.etc =
|
||||||
|
(lib.attrsets.mapAttrs' (name: sp-module: {
|
||||||
|
name = "sp-modules/${name}";
|
||||||
|
value.text = import ./lib/meta.nix { inherit pkgs sp-module; };
|
||||||
|
}) sp-modules)
|
||||||
|
// {
|
||||||
|
suggested-sp-modules.text = builtins.toJSON (builtins.attrNames (builtins.readDir ./sp-modules));
|
||||||
|
};
|
||||||
|
}
|
||||||
)
|
)
|
||||||
];
|
(
|
||||||
disabledModules = [ "services/security/kanidm.nix" ];
|
let
|
||||||
imports = [ ./auth/kanidm.nix ];
|
deepFilter =
|
||||||
}
|
ref: attrset:
|
||||||
selfprivacy-api.nixosModules.default
|
builtins.foldl' (
|
||||||
({ pkgs, lib, ... }: {
|
acc: key:
|
||||||
environment.etc = (lib.attrsets.mapAttrs'
|
if builtins.hasAttr key ref then
|
||||||
(name: sp-module: {
|
let
|
||||||
name = "sp-modules/${name}";
|
value = attrset.${key};
|
||||||
value.text = import ./lib/meta.nix { inherit pkgs sp-module; };
|
refValue = ref.${key};
|
||||||
})
|
in
|
||||||
sp-modules) // {
|
acc
|
||||||
suggested-sp-modules.text = builtins.toJSON (builtins.attrNames (builtins.readDir ./sp-modules));
|
// {
|
||||||
};
|
${key} =
|
||||||
})
|
if builtins.isAttrs value && builtins.isAttrs refValue then deepFilter refValue value else value;
|
||||||
(
|
}
|
||||||
let
|
else
|
||||||
deepFilter = ref: attrset:
|
acc
|
||||||
builtins.foldl'
|
) { } (builtins.attrNames attrset);
|
||||||
(acc: key:
|
in
|
||||||
if builtins.hasAttr key ref then
|
{ options, ... }:
|
||||||
let
|
{
|
||||||
value = attrset.${key};
|
# pass userdata (parsed from JSON) options to selfprivacy module
|
||||||
refValue = ref.${key};
|
selfprivacy = deepFilter options.selfprivacy userdata;
|
||||||
in
|
|
||||||
acc // {
|
|
||||||
${key} =
|
|
||||||
if builtins.isAttrs value && builtins.isAttrs refValue then
|
|
||||||
deepFilter refValue value
|
|
||||||
else
|
|
||||||
value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
acc
|
|
||||||
)
|
|
||||||
{ }
|
|
||||||
(builtins.attrNames attrset);
|
|
||||||
in
|
|
||||||
{ options, ... }: {
|
|
||||||
# pass userdata (parsed from JSON) options to selfprivacy module
|
|
||||||
selfprivacy = deepFilter options.selfprivacy userdata;
|
|
||||||
|
|
||||||
# embed top-level flake source folder into the build
|
# embed top-level flake source folder into the build
|
||||||
environment.etc."selfprivacy/nixos-config-source".source =
|
environment.etc."selfprivacy/nixos-config-source".source = top-level-flake;
|
||||||
top-level-flake;
|
|
||||||
|
|
||||||
# for running "nix search nixpkgs", "nix shell nixpkgs#PKG... etc
|
# for running "nix search nixpkgs", "nix shell nixpkgs#PKG... etc
|
||||||
nix.registry.nixpkgs.flake = nixpkgs;
|
nix.registry.nixpkgs.flake = nixpkgs;
|
||||||
|
|
||||||
# embed commit sha1 for `nixos-version --configuration-revision`
|
# embed commit sha1 for `nixos-version --configuration-revision`
|
||||||
system.configurationRevision = self.rev
|
system.configurationRevision = self.rev or "@${self.lastModifiedDate}"; # for development
|
||||||
or "@${self.lastModifiedDate}"; # for development
|
# TODO assertion to forbid dirty builds caused by top-level-flake
|
||||||
# TODO assertion to forbid dirty builds caused by top-level-flake
|
|
||||||
|
|
||||||
# reset contents of /etc/nixos to match running NixOS generation
|
# reset contents of /etc/nixos to match running NixOS generation
|
||||||
system.activationScripts.selfprivacy-nixos-config-source = ''
|
system.activationScripts.selfprivacy-nixos-config-source = ''
|
||||||
rm -rf /etc/nixos/{*,.[!.]*}
|
rm -rf /etc/nixos/{*,.[!.]*}
|
||||||
cp -r --no-preserve=all ${top-level-flake}/ -T /etc/nixos/
|
cp -r --no-preserve=all ${top-level-flake}/ -T /etc/nixos/
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
++
|
++
|
||||||
# add SP modules, but constrain available config attributes for each
|
# add SP modules, but constrain available config attributes for each
|
||||||
# (TODO revise evaluation performance of the code below)
|
# (TODO revise evaluation performance of the code below)
|
||||||
nixpkgs.lib.attrsets.mapAttrsToList
|
nixpkgs.lib.attrsets.mapAttrsToList (
|
||||||
(name: sp-module: args@{ config, pkgs, ... }:
|
name: sp-module:
|
||||||
let
|
args@{ config, pkgs, ... }:
|
||||||
lib = nixpkgs.lib;
|
let
|
||||||
configPathsNeeded = sp-module.configPathsNeeded or
|
lib = nixpkgs.lib;
|
||||||
(abort "allowed config paths not set for module \"${name}\"");
|
configPathsNeeded =
|
||||||
constrainConfigArgs = args'@{ pkgs, ... }: args' // {
|
sp-module.configPathsNeeded or (abort "allowed config paths not set for module \"${name}\"");
|
||||||
config =
|
constrainConfigArgs =
|
||||||
# TODO use lib.attrsets.mergeAttrsList from nixpkgs 23.05
|
args'@{ pkgs, ... }:
|
||||||
(builtins.foldl' lib.attrsets.recursiveUpdate { }
|
args'
|
||||||
(map
|
// {
|
||||||
(p: lib.attrsets.setAttrByPath p
|
config =
|
||||||
(lib.attrsets.getAttrFromPath p config))
|
# TODO use lib.attrsets.mergeAttrsList from nixpkgs 23.05
|
||||||
configPathsNeeded
|
(
|
||||||
)
|
builtins.foldl' lib.attrsets.recursiveUpdate { } (
|
||||||
);
|
map (p: lib.attrsets.setAttrByPath p (lib.attrsets.getAttrFromPath p config)) configPathsNeeded
|
||||||
};
|
)
|
||||||
constrainImportsArgsRecursive = lib.attrsets.mapAttrsRecursive
|
);
|
||||||
(p: v:
|
};
|
||||||
|
constrainImportsArgsRecursive = lib.attrsets.mapAttrsRecursive (
|
||||||
|
p: v:
|
||||||
# TODO traverse only imports and imports of imports, etc
|
# TODO traverse only imports and imports of imports, etc
|
||||||
# without traversing all attributes
|
# without traversing all attributes
|
||||||
if lib.lists.last p == "imports"
|
if lib.lists.last p == "imports" then
|
||||||
then
|
map (
|
||||||
map
|
m:
|
||||||
(m:
|
(
|
||||||
(args'@{ pkgs, ... }: constrainImportsArgsRecursive
|
args'@{ pkgs, ... }:
|
||||||
(if builtins.isPath m
|
constrainImportsArgsRecursive (
|
||||||
then import m (constrainConfigArgs args')
|
if builtins.isPath m then
|
||||||
|
import m (constrainConfigArgs args')
|
||||||
|
else if builtins.isFunction m then
|
||||||
|
m (constrainConfigArgs args')
|
||||||
else
|
else
|
||||||
if builtins.isFunction m
|
m
|
||||||
then m (constrainConfigArgs args')
|
)
|
||||||
else m))
|
|
||||||
)
|
)
|
||||||
v
|
) v
|
||||||
else v);
|
else
|
||||||
in
|
v
|
||||||
constrainImportsArgsRecursive
|
);
|
||||||
(sp-module.nixosModules.default (constrainConfigArgs args))
|
in
|
||||||
)
|
constrainImportsArgsRecursive (sp-module.nixosModules.default (constrainConfigArgs args))
|
||||||
sp-modules;
|
) sp-modules;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixpkgs-fmt;
|
||||||
formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixpkgs-fmt;
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,9 @@
|
|||||||
{ config, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.selfprivacy;
|
cfg = config.selfprivacy;
|
||||||
dnsCredentialsTemplates = {
|
dnsCredentialsTemplates = {
|
||||||
@@ -27,7 +32,11 @@ in
|
|||||||
acceptTerms = true;
|
acceptTerms = true;
|
||||||
defaults = {
|
defaults = {
|
||||||
email = "${cfg.username}@${cfg.domain}";
|
email = "${cfg.username}@${cfg.domain}";
|
||||||
server = if cfg.dns.useStagingACME then "https://acme-staging-v02.api.letsencrypt.org/directory" else "https://acme-v02.api.letsencrypt.org/directory";
|
server =
|
||||||
|
if cfg.dns.useStagingACME then
|
||||||
|
"https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||||
|
else
|
||||||
|
"https://acme-v02.api.letsencrypt.org/directory";
|
||||||
reloadServices = [ "nginx" ];
|
reloadServices = [ "nginx" ];
|
||||||
dnsResolver = "8.8.8.8:53";
|
dnsResolver = "8.8.8.8:53";
|
||||||
};
|
};
|
||||||
@@ -38,7 +47,9 @@ in
|
|||||||
dnsProvider = lib.strings.toLower cfg.dns.provider;
|
dnsProvider = lib.strings.toLower cfg.dns.provider;
|
||||||
credentialsFile = acme-env-filepath;
|
credentialsFile = acme-env-filepath;
|
||||||
dnsPropagationCheck =
|
dnsPropagationCheck =
|
||||||
! ((lib.elem cfg.dns.provider dnsPropagationCheckExceptions) || cfg.dns.forceDisableDnsPropagationCheck);
|
!(
|
||||||
|
(lib.elem cfg.dns.provider dnsPropagationCheckExceptions) || cfg.dns.forceDisableDnsPropagationCheck
|
||||||
|
);
|
||||||
};
|
};
|
||||||
"root-${cfg.domain}" = {
|
"root-${cfg.domain}" = {
|
||||||
domain = cfg.domain;
|
domain = cfg.domain;
|
||||||
@@ -51,7 +62,10 @@ in
|
|||||||
before = [ "acme-${cfg.domain}.service" ];
|
before = [ "acme-${cfg.domain}.service" ];
|
||||||
requiredBy = [ "acme-${cfg.domain}.service" ];
|
requiredBy = [ "acme-${cfg.domain}.service" ];
|
||||||
serviceConfig.Type = "oneshot";
|
serviceConfig.Type = "oneshot";
|
||||||
path = with pkgs; [ coreutils jq ];
|
path = with pkgs; [
|
||||||
|
coreutils
|
||||||
|
jq
|
||||||
|
];
|
||||||
script = ''
|
script = ''
|
||||||
set -o nounset
|
set -o nounset
|
||||||
|
|
||||||
|
28
lib/meta.nix
28
lib/meta.nix
@@ -1,18 +1,28 @@
|
|||||||
{ sp-module, pkgs }:
|
{ sp-module, pkgs }:
|
||||||
let
|
let
|
||||||
lib = pkgs.lib;
|
lib = pkgs.lib;
|
||||||
options = (pkgs.lib.evalModules { modules = [{ _module.check = false; } sp-module.nixosModules.default]; }).options;
|
options =
|
||||||
|
(pkgs.lib.evalModules {
|
||||||
|
modules = [
|
||||||
|
{ _module.check = false; }
|
||||||
|
sp-module.nixosModules.default
|
||||||
|
];
|
||||||
|
}).options;
|
||||||
# Transform a Nix option to a JSON structure with metadata
|
# Transform a Nix option to a JSON structure with metadata
|
||||||
optionToMeta = (name: option: {
|
optionToMeta = (
|
||||||
name = name;
|
name: option: {
|
||||||
description = if builtins.hasAttr "description" option then option.description else null;
|
name = name;
|
||||||
loc = option.loc;
|
description = if builtins.hasAttr "description" option then option.description else null;
|
||||||
meta = if builtins.hasAttr "meta" option then option.meta else null;
|
loc = option.loc;
|
||||||
default = if builtins.hasAttr "default" option then option.default else null;
|
meta = if builtins.hasAttr "meta" option then option.meta else null;
|
||||||
});
|
default = if builtins.hasAttr "default" option then option.default else null;
|
||||||
|
}
|
||||||
|
);
|
||||||
in
|
in
|
||||||
builtins.toJSON ({
|
builtins.toJSON ({
|
||||||
meta = if builtins.hasAttr "meta" sp-module then sp-module.meta { inherit lib; } else null;
|
meta = if builtins.hasAttr "meta" sp-module then sp-module.meta { inherit lib; } else null;
|
||||||
configPathsNeeded = sp-module.configPathsNeeded;
|
configPathsNeeded = sp-module.configPathsNeeded;
|
||||||
options = pkgs.lib.mapAttrs optionToMeta (builtins.head (lib.mapAttrsToList (name: value: value) options.selfprivacy.modules));
|
options = pkgs.lib.mapAttrs optionToMeta (
|
||||||
|
builtins.head (lib.mapAttrsToList (name: value: value) options.selfprivacy.modules)
|
||||||
|
);
|
||||||
})
|
})
|
||||||
|
@@ -1,6 +1,4 @@
|
|||||||
system:
|
system: _final: _prev: {
|
||||||
_final: _prev:
|
|
||||||
{
|
|
||||||
# Here is a template to bring a specific package from a given nixpkgs commit:
|
# Here is a template to bring a specific package from a given nixpkgs commit:
|
||||||
# PACKAGE_NAME = (builtins.getFlake "github:nixos/nixpkgs/NIXPKGS_COMMIT_SHA1").legacyPackages.${system}.PACKAGE_NAME;
|
# PACKAGE_NAME = (builtins.getFlake "github:nixos/nixpkgs/NIXPKGS_COMMIT_SHA1").legacyPackages.${system}.PACKAGE_NAME;
|
||||||
# Substitute `PACKAGE_NAME` and `NIXPKGS_COMMIT_SHA1` accordingly.
|
# Substitute `PACKAGE_NAME` and `NIXPKGS_COMMIT_SHA1` accordingly.
|
||||||
|
@@ -1,22 +1,34 @@
|
|||||||
{ config, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
sp = config.selfprivacy;
|
sp = config.selfprivacy;
|
||||||
pleroma_location =
|
pleroma_location =
|
||||||
if lib.attrsets.hasAttr "pleroma" sp.modules && lib.attrsets.hasAttr "location" sp.modules.pleroma
|
if
|
||||||
then sp.modules.pleroma.location
|
lib.attrsets.hasAttr "pleroma" sp.modules && lib.attrsets.hasAttr "location" sp.modules.pleroma
|
||||||
else null;
|
then
|
||||||
|
sp.modules.pleroma.location
|
||||||
|
else
|
||||||
|
null;
|
||||||
postgres_location =
|
postgres_location =
|
||||||
if lib.attrsets.hasAttr "postgresql" sp && lib.attrsets.hasAttr "location" sp.postgresql
|
if lib.attrsets.hasAttr "postgresql" sp && lib.attrsets.hasAttr "location" sp.postgresql then
|
||||||
then sp.postgresql.location
|
sp.postgresql.location
|
||||||
else null;
|
else
|
||||||
|
null;
|
||||||
# Priority: postgresql > pleroma
|
# Priority: postgresql > pleroma
|
||||||
location = if postgres_location != null then postgres_location else pleroma_location;
|
location = if postgres_location != null then postgres_location else pleroma_location;
|
||||||
# Active if there is a location
|
# Active if there is a location
|
||||||
enable = location != null;
|
enable = location != null;
|
||||||
pleroma_enabled =
|
pleroma_enabled =
|
||||||
if lib.attrsets.hasAttr "pleroma" sp.modules && lib.attrsets.hasAttr "enable" sp.modules.pleroma
|
if
|
||||||
then sp.modules.pleroma.enable
|
lib.attrsets.hasAttr "pleroma" sp.modules && lib.attrsets.hasAttr "enable" sp.modules.pleroma
|
||||||
else false;
|
then
|
||||||
|
sp.modules.pleroma.enable
|
||||||
|
else
|
||||||
|
false;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
|
@@ -1,4 +1,9 @@
|
|||||||
{ config, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.services.postgresqlUpgrade12to16;
|
cfg = config.services.postgresqlUpgrade12to16;
|
||||||
@@ -62,72 +67,70 @@ in
|
|||||||
|
|
||||||
ExecStartPre =
|
ExecStartPre =
|
||||||
# Stop Pleroma only if pleromaEnabled is true
|
# Stop Pleroma only if pleromaEnabled is true
|
||||||
optional cfg.pleromaEnabled "${pkgs.writeShellScript "postgresql-upgrade12to16-pre.sh" ''
|
optional cfg.pleromaEnabled
|
||||||
if [ -d "${cfg.dataDir12}" ] && [ ! -d "${cfg.dataDir16}" ]; then
|
"${pkgs.writeShellScript "postgresql-upgrade12to16-pre.sh" ''
|
||||||
${pkgs.systemd}/bin/systemctl stop pleroma.service
|
if [ -d "${cfg.dataDir12}" ] && [ ! -d "${cfg.dataDir16}" ]; then
|
||||||
fi
|
${pkgs.systemd}/bin/systemctl stop pleroma.service
|
||||||
''
|
fi
|
||||||
}";
|
''}";
|
||||||
|
|
||||||
ExecStart = "${pkgs.writeShellScript "postgresql-upgrade12to16.sh" ''
|
ExecStart = "${pkgs.writeShellScript "postgresql-upgrade12to16.sh" ''
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
oldDataDir="${cfg.dataDir12}"
|
oldDataDir="${cfg.dataDir12}"
|
||||||
newDataDir="${cfg.dataDir16}"
|
newDataDir="${cfg.dataDir16}"
|
||||||
|
|
||||||
# Only upgrade if old data directory exists, and the new one doesn't yet
|
# Only upgrade if old data directory exists, and the new one doesn't yet
|
||||||
if [ -d "$oldDataDir" ] && [ ! -d "$newDataDir" ]; then
|
if [ -d "$oldDataDir" ] && [ ! -d "$newDataDir" ]; then
|
||||||
echo "Detected PostgreSQL 12 data directory at $oldDataDir"
|
echo "Detected PostgreSQL 12 data directory at $oldDataDir"
|
||||||
echo "Upgrading to PostgreSQL 16 data directory at $newDataDir"
|
echo "Upgrading to PostgreSQL 16 data directory at $newDataDir"
|
||||||
|
|
||||||
# Stop the old PostgreSQL if it's running
|
# Stop the old PostgreSQL if it's running
|
||||||
if systemctl is-active --quiet postgresql.service; then
|
if systemctl is-active --quiet postgresql.service; then
|
||||||
systemctl stop postgresql.service
|
systemctl stop postgresql.service
|
||||||
fi
|
|
||||||
|
|
||||||
# Create the new data directory (if not already present)
|
|
||||||
mkdir -p "$newDataDir"
|
|
||||||
chown -R postgres:postgres "$(dirname "$newDataDir")"
|
|
||||||
|
|
||||||
# Create a temporary working directory
|
|
||||||
tempDir=$(mktemp -d)
|
|
||||||
chown -R postgres:postgres "$tempDir"
|
|
||||||
trap 'rm -rf "$tempDir"' EXIT
|
|
||||||
|
|
||||||
# Change to the temporary working directory
|
|
||||||
cd "$tempDir"
|
|
||||||
|
|
||||||
# Initialize the new PostgreSQL 16 data directory
|
|
||||||
${pkgs.sudo}/bin/sudo -u postgres ${pkgs.postgresql_16.out}/bin/initdb -D "$newDataDir" -U postgres
|
|
||||||
|
|
||||||
# Run pg_upgrade as the postgres user
|
|
||||||
${pkgs.sudo}/bin/sudo -u postgres ${pkgs.postgresql_16.out}/bin/pg_upgrade \
|
|
||||||
--old-datadir "$oldDataDir" \
|
|
||||||
--new-datadir "$newDataDir" \
|
|
||||||
--old-bindir ${pkgs.postgresql_12.out}/bin \
|
|
||||||
--new-bindir ${pkgs.postgresql_16.out}/bin \
|
|
||||||
--jobs "$(nproc)" \
|
|
||||||
--link \
|
|
||||||
--verbose
|
|
||||||
|
|
||||||
touch "$newDataDir/.sp_migrated"
|
|
||||||
|
|
||||||
echo "PostgreSQL upgrade from 12 to 16 completed."
|
|
||||||
else
|
|
||||||
echo "No PostgreSQL 12 data directory detected or already upgraded. Skipping."
|
|
||||||
fi
|
fi
|
||||||
''}";
|
|
||||||
|
# Create the new data directory (if not already present)
|
||||||
|
mkdir -p "$newDataDir"
|
||||||
|
chown -R postgres:postgres "$(dirname "$newDataDir")"
|
||||||
|
|
||||||
|
# Create a temporary working directory
|
||||||
|
tempDir=$(mktemp -d)
|
||||||
|
chown -R postgres:postgres "$tempDir"
|
||||||
|
trap 'rm -rf "$tempDir"' EXIT
|
||||||
|
|
||||||
|
# Change to the temporary working directory
|
||||||
|
cd "$tempDir"
|
||||||
|
|
||||||
|
# Initialize the new PostgreSQL 16 data directory
|
||||||
|
${pkgs.sudo}/bin/sudo -u postgres ${pkgs.postgresql_16.out}/bin/initdb -D "$newDataDir" -U postgres
|
||||||
|
|
||||||
|
# Run pg_upgrade as the postgres user
|
||||||
|
${pkgs.sudo}/bin/sudo -u postgres ${pkgs.postgresql_16.out}/bin/pg_upgrade \
|
||||||
|
--old-datadir "$oldDataDir" \
|
||||||
|
--new-datadir "$newDataDir" \
|
||||||
|
--old-bindir ${pkgs.postgresql_12.out}/bin \
|
||||||
|
--new-bindir ${pkgs.postgresql_16.out}/bin \
|
||||||
|
--jobs "$(nproc)" \
|
||||||
|
--link \
|
||||||
|
--verbose
|
||||||
|
|
||||||
|
touch "$newDataDir/.sp_migrated"
|
||||||
|
|
||||||
|
echo "PostgreSQL upgrade from 12 to 16 completed."
|
||||||
|
else
|
||||||
|
echo "No PostgreSQL 12 data directory detected or already upgraded. Skipping."
|
||||||
|
fi
|
||||||
|
''}";
|
||||||
|
|
||||||
# Start Pleroma only if pleromaEnabled is true
|
# Start Pleroma only if pleromaEnabled is true
|
||||||
ExecStartPost =
|
ExecStartPost = optional cfg.pleromaEnabled "${pkgs.writeShellScript "postgresql-upgrade12to16-post.sh" ''
|
||||||
optional cfg.pleromaEnabled "${pkgs.writeShellScript "postgresql-upgrade12to16-post.sh" ''
|
if test -e "${cfg.dataDir16}/.sp_migrated"; then
|
||||||
if test -e "${cfg.dataDir16}/.sp_migrated"; then
|
${pkgs.systemd}/bin/systemctl start --no-block pleroma.service
|
||||||
${pkgs.systemd}/bin/systemctl start --no-block pleroma.service
|
|
||||||
|
|
||||||
rm -f "${cfg.dataDir16}/.sp_migrated"
|
rm -f "${cfg.dataDir16}/.sp_migrated"
|
||||||
fi
|
fi
|
||||||
''
|
''}";
|
||||||
}";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -5,13 +5,8 @@ in
|
|||||||
# FIXME do we really want to delete passwords on module deactivation!?
|
# FIXME do we really want to delete passwords on module deactivation!?
|
||||||
{
|
{
|
||||||
config = lib.mkIf (!sp.modules.bitwarden.enable) {
|
config = lib.mkIf (!sp.modules.bitwarden.enable) {
|
||||||
system.activationScripts.bitwarden =
|
system.activationScripts.bitwarden = lib.trivial.warn ("bitwarden service is disabled, ${bitwarden-env} will be removed!") ''
|
||||||
lib.trivial.warn
|
rm -f -v ${bitwarden-env}
|
||||||
(
|
'';
|
||||||
"bitwarden service is disabled, ${bitwarden-env} will be removed!"
|
|
||||||
)
|
|
||||||
''
|
|
||||||
rm -f -v ${bitwarden-env}
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
config:
|
config: {
|
||||||
{
|
|
||||||
sp = config.selfprivacy;
|
sp = config.selfprivacy;
|
||||||
bitwarden-env = "/var/lib/bitwarden/.env";
|
bitwarden-env = "/var/lib/bitwarden/.env";
|
||||||
}
|
}
|
||||||
|
@@ -1,34 +1,41 @@
|
|||||||
{
|
{
|
||||||
description = "PoC SP module for Bitwarden password management solution";
|
description = "PoC SP module for Bitwarden password management solution";
|
||||||
|
|
||||||
outputs = { self }: {
|
outputs =
|
||||||
nixosModules.default = _:
|
{ self }:
|
||||||
{ imports = [ ./module.nix ./cleanup-module.nix ]; };
|
{
|
||||||
configPathsNeeded =
|
nixosModules.default = _: {
|
||||||
builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
|
imports = [
|
||||||
meta = { lib, ... }: {
|
./module.nix
|
||||||
spModuleSchemaVersion = 1;
|
./cleanup-module.nix
|
||||||
id = "bitwarden";
|
];
|
||||||
name = "Bitwarden";
|
};
|
||||||
description = "Bitwarden is a password manager.";
|
configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
|
||||||
svgIcon = builtins.readFile ./icon.svg;
|
meta =
|
||||||
isMovable = true;
|
{ lib, ... }:
|
||||||
isRequired = false;
|
{
|
||||||
backupDescription = "Password database, encryption certificate and attachments.";
|
spModuleSchemaVersion = 1;
|
||||||
systemdServices = [
|
id = "bitwarden";
|
||||||
"vaultwarden.service"
|
name = "Bitwarden";
|
||||||
];
|
description = "Bitwarden is a password manager.";
|
||||||
user = "vaultwarden";
|
svgIcon = builtins.readFile ./icon.svg;
|
||||||
folders = [
|
isMovable = true;
|
||||||
"/var/lib/bitwarden"
|
isRequired = false;
|
||||||
"/var/lib/bitwarden_rs"
|
backupDescription = "Password database, encryption certificate and attachments.";
|
||||||
];
|
systemdServices = [
|
||||||
license = [
|
"vaultwarden.service"
|
||||||
lib.licenses.agpl3Only
|
];
|
||||||
];
|
user = "vaultwarden";
|
||||||
homepage = "https://github.com/dani-garcia/vaultwarden";
|
folders = [
|
||||||
sourcePage = "https://github.com/dani-garcia/vaultwarden";
|
"/var/lib/bitwarden"
|
||||||
supportLevel = "normal";
|
"/var/lib/bitwarden_rs"
|
||||||
|
];
|
||||||
|
license = [
|
||||||
|
lib.licenses.agpl3Only
|
||||||
|
];
|
||||||
|
homepage = "https://github.com/dani-garcia/vaultwarden";
|
||||||
|
sourcePage = "https://github.com/dani-garcia/vaultwarden";
|
||||||
|
supportLevel = "normal";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,9 @@
|
|||||||
{ config, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
secrets-filepath = "/etc/selfprivacy/secrets.json";
|
secrets-filepath = "/etc/selfprivacy/secrets.json";
|
||||||
backup-dir = "/var/lib/bitwarden/backup";
|
backup-dir = "/var/lib/bitwarden/backup";
|
||||||
@@ -7,65 +12,77 @@ let
|
|||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.selfprivacy.modules.bitwarden = {
|
options.selfprivacy.modules.bitwarden = {
|
||||||
enable = (lib.mkOption {
|
enable =
|
||||||
default = false;
|
(lib.mkOption {
|
||||||
type = lib.types.bool;
|
default = false;
|
||||||
description = "Enable Vaultwarden";
|
type = lib.types.bool;
|
||||||
}) // {
|
description = "Enable Vaultwarden";
|
||||||
meta = {
|
})
|
||||||
type = "enable";
|
// {
|
||||||
|
meta = {
|
||||||
|
type = "enable";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
location =
|
||||||
location = (lib.mkOption {
|
(lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Vaultwarden location";
|
description = "Vaultwarden location";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "location";
|
meta = {
|
||||||
|
type = "location";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
subdomain =
|
||||||
subdomain = (lib.mkOption {
|
(lib.mkOption {
|
||||||
default = "password";
|
default = "password";
|
||||||
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
||||||
description = "Subdomain";
|
description = "Subdomain";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
widget = "subdomain";
|
meta = {
|
||||||
type = "string";
|
widget = "subdomain";
|
||||||
regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
type = "string";
|
||||||
weight = 0;
|
regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
||||||
|
weight = 0;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
signupsAllowed =
|
||||||
signupsAllowed = (lib.mkOption {
|
(lib.mkOption {
|
||||||
default = true;
|
default = true;
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
description = "Allow new user signups";
|
description = "Allow new user signups";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "bool";
|
meta = {
|
||||||
weight = 1;
|
type = "bool";
|
||||||
|
weight = 1;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
sendsAllowed =
|
||||||
sendsAllowed = (lib.mkOption {
|
(lib.mkOption {
|
||||||
default = true;
|
default = true;
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
description = "Allow users to use Bitwarden Send";
|
description = "Allow users to use Bitwarden Send";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "bool";
|
meta = {
|
||||||
weight = 2;
|
type = "bool";
|
||||||
|
weight = 2;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
emergencyAccessAllowed =
|
||||||
emergencyAccessAllowed = (lib.mkOption {
|
(lib.mkOption {
|
||||||
default = true;
|
default = true;
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
description = "Allow users to enable Emergency Access";
|
description = "Allow users to enable Emergency Access";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "bool";
|
meta = {
|
||||||
weight = 3;
|
type = "bool";
|
||||||
|
weight = 3;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf config.selfprivacy.modules.bitwarden.enable {
|
config = lib.mkIf config.selfprivacy.modules.bitwarden.enable {
|
||||||
@@ -118,7 +135,10 @@ in
|
|||||||
before = [ "vaultwarden.service" ];
|
before = [ "vaultwarden.service" ];
|
||||||
requiredBy = [ "vaultwarden.service" ];
|
requiredBy = [ "vaultwarden.service" ];
|
||||||
serviceConfig.Type = "oneshot";
|
serviceConfig.Type = "oneshot";
|
||||||
path = with pkgs; [ coreutils jq ];
|
path = with pkgs; [
|
||||||
|
coreutils
|
||||||
|
jq
|
||||||
|
];
|
||||||
script = ''
|
script = ''
|
||||||
set -o nounset
|
set -o nounset
|
||||||
|
|
||||||
|
@@ -1,35 +1,38 @@
|
|||||||
{
|
{
|
||||||
description = "PoC SP module for Gitea forge service";
|
description = "PoC SP module for Gitea forge service";
|
||||||
|
|
||||||
outputs = { self }: {
|
outputs =
|
||||||
nixosModules.default = import ./module.nix;
|
{ self }:
|
||||||
configPathsNeeded =
|
{
|
||||||
builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
|
nixosModules.default = import ./module.nix;
|
||||||
meta = { lib, ... }: {
|
configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
|
||||||
spModuleSchemaVersion = 1;
|
meta =
|
||||||
id = "gitea";
|
{ lib, ... }:
|
||||||
name = "Forgejo";
|
{
|
||||||
description = "Forgejo is a Git forge.";
|
spModuleSchemaVersion = 1;
|
||||||
svgIcon = builtins.readFile ./icon.svg;
|
id = "gitea";
|
||||||
isMovable = true;
|
name = "Forgejo";
|
||||||
isRequired = false;
|
description = "Forgejo is a Git forge.";
|
||||||
backupDescription = "Git repositories, database and user data.";
|
svgIcon = builtins.readFile ./icon.svg;
|
||||||
systemdServices = [
|
isMovable = true;
|
||||||
"forgejo.service"
|
isRequired = false;
|
||||||
];
|
backupDescription = "Git repositories, database and user data.";
|
||||||
folders = [
|
systemdServices = [
|
||||||
"/var/lib/gitea"
|
"forgejo.service"
|
||||||
];
|
];
|
||||||
license = [
|
folders = [
|
||||||
lib.licenses.gpl3Plus
|
"/var/lib/gitea"
|
||||||
];
|
];
|
||||||
homepage = "https://forgejo.org";
|
license = [
|
||||||
sourcePage = "https://codeberg.org/forgejo/forgejo";
|
lib.licenses.gpl3Plus
|
||||||
supportLevel = "normal";
|
];
|
||||||
sso = {
|
homepage = "https://forgejo.org";
|
||||||
userGroup = "sp.gitea.users";
|
sourcePage = "https://codeberg.org/forgejo/forgejo";
|
||||||
adminGroup = "sp.gitea.admins";
|
supportLevel = "normal";
|
||||||
};
|
sso = {
|
||||||
|
userGroup = "sp.gitea.users";
|
||||||
|
adminGroup = "sp.gitea.admins";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,12 @@
|
|||||||
{ config, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
sp = config.selfprivacy;
|
sp = config.selfprivacy;
|
||||||
stateDir =
|
stateDir = if sp.useBinds then "/volumes/${cfg.location}/gitea" else "/var/lib/gitea";
|
||||||
if sp.useBinds
|
|
||||||
then "/volumes/${cfg.location}/gitea"
|
|
||||||
else "/var/lib/gitea";
|
|
||||||
cfg = sp.modules.gitea;
|
cfg = sp.modules.gitea;
|
||||||
themes = [
|
themes = [
|
||||||
"forgejo-auto"
|
"forgejo-auto"
|
||||||
@@ -18,8 +20,7 @@ let
|
|||||||
oauthClientID = "forgejo";
|
oauthClientID = "forgejo";
|
||||||
auth-passthru = config.selfprivacy.passthru.auth;
|
auth-passthru = config.selfprivacy.passthru.auth;
|
||||||
oauth2-provider-name = auth-passthru.oauth2-provider-name;
|
oauth2-provider-name = auth-passthru.oauth2-provider-name;
|
||||||
redirect-uri =
|
redirect-uri = "https://${cfg.subdomain}.${sp.domain}/user/oauth2/${oauth2-provider-name}/callback";
|
||||||
"https://${cfg.subdomain}.${sp.domain}/user/oauth2/${oauth2-provider-name}/callback";
|
|
||||||
oauthDiscoveryURL = auth-passthru.oauth2-discovery-url oauthClientID;
|
oauthDiscoveryURL = auth-passthru.oauth2-discovery-url oauthClientID;
|
||||||
|
|
||||||
# SelfPrivacy uses SP Module ID to identify the group!
|
# SelfPrivacy uses SP Module ID to identify the group!
|
||||||
@@ -30,381 +31,401 @@ let
|
|||||||
linuxGroupOfService = "gitea";
|
linuxGroupOfService = "gitea";
|
||||||
forgejoPackage = pkgs.forgejo;
|
forgejoPackage = pkgs.forgejo;
|
||||||
|
|
||||||
serviceAccountTokenFP =
|
serviceAccountTokenFP = auth-passthru.mkServiceAccountTokenFP linuxGroupOfService;
|
||||||
auth-passthru.mkServiceAccountTokenFP linuxGroupOfService;
|
oauthClientSecretFP = auth-passthru.mkOAuth2ClientSecretFP linuxGroupOfService;
|
||||||
oauthClientSecretFP =
|
|
||||||
auth-passthru.mkOAuth2ClientSecretFP linuxGroupOfService;
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.selfprivacy.modules.gitea = {
|
options.selfprivacy.modules.gitea = {
|
||||||
enable = (lib.mkOption {
|
enable =
|
||||||
default = false;
|
(lib.mkOption {
|
||||||
type = lib.types.bool;
|
default = false;
|
||||||
description = "Enable Forgejo";
|
type = lib.types.bool;
|
||||||
}) // {
|
description = "Enable Forgejo";
|
||||||
meta = {
|
})
|
||||||
type = "enable";
|
// {
|
||||||
|
meta = {
|
||||||
|
type = "enable";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
location =
|
||||||
location = (lib.mkOption {
|
(lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Forgejo location";
|
description = "Forgejo location";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "location";
|
meta = {
|
||||||
|
type = "location";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
subdomain =
|
||||||
subdomain = (lib.mkOption {
|
(lib.mkOption {
|
||||||
default = "git";
|
default = "git";
|
||||||
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
||||||
description = "Subdomain";
|
description = "Subdomain";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
widget = "subdomain";
|
meta = {
|
||||||
type = "string";
|
widget = "subdomain";
|
||||||
regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
type = "string";
|
||||||
weight = 0;
|
regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
||||||
|
weight = 0;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
appName =
|
||||||
appName = (lib.mkOption {
|
(lib.mkOption {
|
||||||
default = "SelfPrivacy git Service";
|
default = "SelfPrivacy git Service";
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "The name displayed in the web interface";
|
description = "The name displayed in the web interface";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "string";
|
meta = {
|
||||||
weight = 1;
|
type = "string";
|
||||||
|
weight = 1;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
enableLfs =
|
||||||
enableLfs = (lib.mkOption {
|
(lib.mkOption {
|
||||||
default = true;
|
default = true;
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
description = "Enable Git LFS";
|
description = "Enable Git LFS";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "bool";
|
meta = {
|
||||||
weight = 2;
|
type = "bool";
|
||||||
|
weight = 2;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
forcePrivate =
|
||||||
forcePrivate = (lib.mkOption {
|
(lib.mkOption {
|
||||||
default = false;
|
default = false;
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
description = "Force all new repositories to be private";
|
description = "Force all new repositories to be private";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "bool";
|
meta = {
|
||||||
weight = 3;
|
type = "bool";
|
||||||
|
weight = 3;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
disableRegistration =
|
||||||
disableRegistration = (lib.mkOption {
|
(lib.mkOption {
|
||||||
default = false;
|
default = false;
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
description = "Disable registration of new users";
|
description = "Disable registration of new users";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "bool";
|
meta = {
|
||||||
weight = 4;
|
type = "bool";
|
||||||
|
weight = 4;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
requireSigninView =
|
||||||
requireSigninView = (lib.mkOption {
|
(lib.mkOption {
|
||||||
default = false;
|
default = false;
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
description = "Force users to log in to view any page";
|
description = "Force users to log in to view any page";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "bool";
|
meta = {
|
||||||
weight = 5;
|
type = "bool";
|
||||||
|
weight = 5;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
defaultTheme =
|
||||||
defaultTheme = (lib.mkOption {
|
(lib.mkOption {
|
||||||
default = "forgejo-auto";
|
default = "forgejo-auto";
|
||||||
type = lib.types.enum themes;
|
type = lib.types.enum themes;
|
||||||
description = "Default theme";
|
description = "Default theme";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "enum";
|
meta = {
|
||||||
options = themes;
|
type = "enum";
|
||||||
weight = 6;
|
options = themes;
|
||||||
|
weight = 6;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
enableSso =
|
||||||
enableSso = (lib.mkOption {
|
(lib.mkOption {
|
||||||
default = false;
|
default = false;
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
description = "Enable Single Sign-On";
|
description = "Enable Single Sign-On";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "bool";
|
meta = {
|
||||||
weight = 7;
|
type = "bool";
|
||||||
|
weight = 7;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
debug =
|
||||||
debug = (lib.mkOption {
|
(lib.mkOption {
|
||||||
default = false;
|
default = false;
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
description = "Enable debug logging";
|
description = "Enable debug logging";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "bool";
|
meta = {
|
||||||
weight = 8;
|
type = "bool";
|
||||||
|
weight = 8;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable (lib.mkMerge [
|
config = lib.mkIf cfg.enable (
|
||||||
{
|
lib.mkMerge [
|
||||||
assertions = [
|
{
|
||||||
{
|
assertions = [
|
||||||
assertion = cfg.enableSso -> sp.sso.enable;
|
{
|
||||||
message =
|
assertion = cfg.enableSso -> sp.sso.enable;
|
||||||
"SSO cannot be enabled for Forgejo when SSO is disabled globally.";
|
message = "SSO cannot be enabled for Forgejo when SSO is disabled globally.";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
fileSystems = lib.mkIf sp.useBinds {
|
fileSystems = lib.mkIf sp.useBinds {
|
||||||
"/var/lib/gitea" = {
|
"/var/lib/gitea" = {
|
||||||
device = "/volumes/${cfg.location}/gitea";
|
device = "/volumes/${cfg.location}/gitea";
|
||||||
options = [ "bind" ];
|
options = [ "bind" ];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
services.gitea.enable = false;
|
||||||
services.gitea.enable = false;
|
services.forgejo = {
|
||||||
services.forgejo = {
|
enable = true;
|
||||||
enable = true;
|
package = forgejoPackage;
|
||||||
package = forgejoPackage;
|
inherit stateDir;
|
||||||
inherit stateDir;
|
|
||||||
user = linuxUserOfService;
|
|
||||||
group = linuxGroupOfService;
|
|
||||||
database = {
|
|
||||||
type = "sqlite3";
|
|
||||||
host = "127.0.0.1";
|
|
||||||
name = "gitea";
|
|
||||||
user = linuxUserOfService;
|
user = linuxUserOfService;
|
||||||
path = "${stateDir}/data/gitea.db";
|
group = linuxGroupOfService;
|
||||||
createDatabase = true;
|
database = {
|
||||||
|
type = "sqlite3";
|
||||||
|
host = "127.0.0.1";
|
||||||
|
name = "gitea";
|
||||||
|
user = linuxUserOfService;
|
||||||
|
path = "${stateDir}/data/gitea.db";
|
||||||
|
createDatabase = true;
|
||||||
|
};
|
||||||
|
# ssh = {
|
||||||
|
# enable = true;
|
||||||
|
# clonePort = 22;
|
||||||
|
# };
|
||||||
|
lfs = {
|
||||||
|
enable = cfg.enableLfs;
|
||||||
|
contentDir = "${stateDir}/lfs";
|
||||||
|
};
|
||||||
|
repositoryRoot = "${stateDir}/repositories";
|
||||||
|
# cookieSecure = true;
|
||||||
|
settings = {
|
||||||
|
DEFAULT = {
|
||||||
|
APP_NAME = "${cfg.appName}";
|
||||||
|
};
|
||||||
|
server = {
|
||||||
|
DOMAIN = "${cfg.subdomain}.${sp.domain}";
|
||||||
|
ROOT_URL = "https://${cfg.subdomain}.${sp.domain}/";
|
||||||
|
HTTP_ADDR = "0.0.0.0";
|
||||||
|
HTTP_PORT = 3000;
|
||||||
|
};
|
||||||
|
mailer = {
|
||||||
|
ENABLED = false;
|
||||||
|
};
|
||||||
|
ui = {
|
||||||
|
DEFAULT_THEME = cfg.defaultTheme;
|
||||||
|
SHOW_USER_EMAIL = false;
|
||||||
|
};
|
||||||
|
picture = {
|
||||||
|
DISABLE_GRAVATAR = true;
|
||||||
|
};
|
||||||
|
admin = {
|
||||||
|
ENABLE_KANBAN_BOARD = true;
|
||||||
|
};
|
||||||
|
repository = {
|
||||||
|
FORCE_PRIVATE = cfg.forcePrivate;
|
||||||
|
};
|
||||||
|
session = {
|
||||||
|
COOKIE_SECURE = true;
|
||||||
|
};
|
||||||
|
log = {
|
||||||
|
ROOT_PATH = "${stateDir}/log";
|
||||||
|
LEVEL = if cfg.debug then "Warn" else "Trace";
|
||||||
|
};
|
||||||
|
service = {
|
||||||
|
DISABLE_REGISTRATION = cfg.disableRegistration;
|
||||||
|
REQUIRE_SIGNIN_VIEW = cfg.requireSigninView;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
# ssh = {
|
|
||||||
# enable = true;
|
users.users.gitea = {
|
||||||
# clonePort = 22;
|
home = "${stateDir}";
|
||||||
# };
|
useDefaultShell = true;
|
||||||
lfs = {
|
group = linuxGroupOfService;
|
||||||
enable = cfg.enableLfs;
|
isSystemUser = true;
|
||||||
contentDir = "${stateDir}/lfs";
|
|
||||||
};
|
};
|
||||||
repositoryRoot = "${stateDir}/repositories";
|
users.groups.${linuxGroupOfService} = { };
|
||||||
# cookieSecure = true;
|
services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = {
|
||||||
settings = {
|
useACMEHost = sp.domain;
|
||||||
DEFAULT = {
|
forceSSL = true;
|
||||||
APP_NAME = "${cfg.appName}";
|
extraConfig = ''
|
||||||
|
add_header Strict-Transport-Security $hsts_header;
|
||||||
|
#add_header Content-Security-Policy "script-src 'self'; object-src 'none'; base-uri 'none';" always;
|
||||||
|
add_header 'Referrer-Policy' 'origin-when-cross-origin';
|
||||||
|
add_header X-Frame-Options DENY;
|
||||||
|
add_header X-Content-Type-Options nosniff;
|
||||||
|
add_header X-XSS-Protection "1; mode=block";
|
||||||
|
proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict";
|
||||||
|
'';
|
||||||
|
locations = {
|
||||||
|
"/" = {
|
||||||
|
proxyPass = "http://127.0.0.1:3000";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
server = {
|
};
|
||||||
DOMAIN = "${cfg.subdomain}.${sp.domain}";
|
systemd = {
|
||||||
ROOT_URL = "https://${cfg.subdomain}.${sp.domain}/";
|
services.forgejo = {
|
||||||
HTTP_ADDR = "0.0.0.0";
|
unitConfig.RequiresMountsFor = lib.mkIf sp.useBinds "/volumes/${cfg.location}/gitea";
|
||||||
HTTP_PORT = 3000;
|
serviceConfig = {
|
||||||
|
Slice = "gitea.slice";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
mailer = {
|
slices.gitea = {
|
||||||
ENABLED = false;
|
description = "Forgejo service slice";
|
||||||
};
|
|
||||||
ui = {
|
|
||||||
DEFAULT_THEME = cfg.defaultTheme;
|
|
||||||
SHOW_USER_EMAIL = false;
|
|
||||||
};
|
|
||||||
picture = {
|
|
||||||
DISABLE_GRAVATAR = true;
|
|
||||||
};
|
|
||||||
admin = {
|
|
||||||
ENABLE_KANBAN_BOARD = true;
|
|
||||||
};
|
|
||||||
repository = {
|
|
||||||
FORCE_PRIVATE = cfg.forcePrivate;
|
|
||||||
};
|
|
||||||
session = {
|
|
||||||
COOKIE_SECURE = true;
|
|
||||||
};
|
|
||||||
log = {
|
|
||||||
ROOT_PATH = "${stateDir}/log";
|
|
||||||
LEVEL = if cfg.debug then "Warn" else "Trace";
|
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
# the following part is active only when enableSso = true
|
||||||
|
(lib.mkIf is-auth-enabled {
|
||||||
|
services.forgejo.settings = {
|
||||||
|
auth.DISABLE_LOGIN_FORM = true;
|
||||||
service = {
|
service = {
|
||||||
DISABLE_REGISTRATION = cfg.disableRegistration;
|
DISABLE_REGISTRATION = cfg.disableRegistration;
|
||||||
REQUIRE_SIGNIN_VIEW = cfg.requireSigninView;
|
REQUIRE_SIGNIN_VIEW = cfg.requireSigninView;
|
||||||
|
ALLOW_ONLY_EXTERNAL_REGISTRATION = true;
|
||||||
|
SHOW_REGISTRATION_BUTTON = false;
|
||||||
|
ENABLE_BASIC_AUTHENTICATION = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
# disallow explore page and access to private repositories, but allow public
|
||||||
|
"service.explore".REQUIRE_SIGNIN_VIEW = true;
|
||||||
|
|
||||||
|
# TODO control via selfprivacy parameter
|
||||||
|
# "service.explore".DISABLE_USERS_PAGE = true;
|
||||||
|
|
||||||
|
oauth2_client = {
|
||||||
|
REDIRECT_URI = redirect-uri;
|
||||||
|
ACCOUNT_LINKING = "auto";
|
||||||
|
ENABLE_AUTO_REGISTRATION = true;
|
||||||
|
OPENID_CONNECT_SCOPES = "email openid profile";
|
||||||
|
};
|
||||||
|
# doesn't work if LDAP auth source is not active!
|
||||||
|
"cron.sync_external_users" = {
|
||||||
|
ENABLED = true;
|
||||||
|
RUN_AT_START = true;
|
||||||
|
NOTICE_ON_SUCCESS = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
systemd.services.forgejo = {
|
||||||
|
preStart =
|
||||||
|
let
|
||||||
|
waitForURL = url: maxRetries: delaySec: ''
|
||||||
|
for ((i=1; i<=${toString maxRetries}; i++))
|
||||||
|
do
|
||||||
|
if ${lib.getExe pkgs.curl} -X GET --silent --fail "${url}" > /dev/null
|
||||||
|
then
|
||||||
|
echo "${url} responds to GET HTTP request (attempt #$i)"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "${url} does not respond to GET HTTP request (attempt #$i)"
|
||||||
|
echo sleeping for ${toString delaySec} seconds
|
||||||
|
fi
|
||||||
|
sleep ${toString delaySec}
|
||||||
|
done
|
||||||
|
if [[ "$i" -gt "${toString maxRetries}" ]]
|
||||||
|
then
|
||||||
|
echo "error, max attempts to access "${url}" have been used unsuccessfully!"
|
||||||
|
exit 124
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
users.users.gitea = {
|
exe = lib.getExe config.services.forgejo.package;
|
||||||
home = "${stateDir}";
|
# FIXME skip-tls-verify, bind-password
|
||||||
useDefaultShell = true;
|
ldapConfigArgs = ''
|
||||||
group = linuxGroupOfService;
|
--name LDAP \
|
||||||
isSystemUser = true;
|
--active \
|
||||||
};
|
--security-protocol LDAPS \
|
||||||
users.groups.${linuxGroupOfService} = { };
|
--skip-tls-verify \
|
||||||
services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = {
|
--host '${auth-passthru.ldap-host}' \
|
||||||
useACMEHost = sp.domain;
|
--port '${toString auth-passthru.ldap-port}' \
|
||||||
forceSSL = true;
|
--user-search-base '${auth-passthru.ldap-base-dn}' \
|
||||||
extraConfig = ''
|
--user-filter '(&(class=person)(memberof=${usersGroup})(name=%s))' \
|
||||||
add_header Strict-Transport-Security $hsts_header;
|
--admin-filter '(&(class=person)(memberof=${adminsGroup})' \
|
||||||
#add_header Content-Security-Policy "script-src 'self'; object-src 'none'; base-uri 'none';" always;
|
--username-attribute name \
|
||||||
add_header 'Referrer-Policy' 'origin-when-cross-origin';
|
--firstname-attribute name \
|
||||||
add_header X-Frame-Options DENY;
|
--surname-attribute displayname \
|
||||||
add_header X-Content-Type-Options nosniff;
|
--email-attribute mail \
|
||||||
add_header X-XSS-Protection "1; mode=block";
|
--public-ssh-key-attribute sshPublicKey \
|
||||||
proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict";
|
--bind-dn 'dn=token' \
|
||||||
'';
|
--bind-password "$(< ${serviceAccountTokenFP})" \
|
||||||
locations = {
|
--synchronize-users
|
||||||
"/" = {
|
'';
|
||||||
proxyPass = "http://127.0.0.1:3000";
|
oauthConfigArgs = ''
|
||||||
|
--name "${oauth2-provider-name}" \
|
||||||
|
--provider openidConnect \
|
||||||
|
--key forgejo \
|
||||||
|
--secret "$(< ${oauthClientSecretFP})" \
|
||||||
|
--group-claim-name groups \
|
||||||
|
--admin-group admins \
|
||||||
|
--auto-discover-url '${oauthDiscoveryURL}'
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
lib.mkMerge [
|
||||||
|
(waitForURL oauthDiscoveryURL 10 10)
|
||||||
|
(lib.mkAfter ''
|
||||||
|
set -o xtrace
|
||||||
|
|
||||||
|
# Check if LDAP is already configured
|
||||||
|
ldap_line="$(${exe} admin auth list | grep LDAP | head -n 1)"
|
||||||
|
|
||||||
|
if [[ -n "$ldap_line" ]]; then
|
||||||
|
# update ldap config
|
||||||
|
id="$(echo "$ldap_line" | ${pkgs.gawk}/bin/awk '{print $1}')"
|
||||||
|
${exe} admin auth update-ldap --id "$id" ${ldapConfigArgs}
|
||||||
|
else
|
||||||
|
# initially configure ldap
|
||||||
|
${exe} admin auth add-ldap ${ldapConfigArgs}
|
||||||
|
fi
|
||||||
|
|
||||||
|
oauth_line="$(${exe} admin auth list | grep "${oauth2-provider-name}" | head -n 1)"
|
||||||
|
if [[ -n "$oauth_line" ]]; then
|
||||||
|
id="$(echo "$oauth_line" | ${pkgs.gawk}/bin/awk '{print $1}')"
|
||||||
|
${exe} admin auth update-oauth --id "$id" ${oauthConfigArgs}
|
||||||
|
else
|
||||||
|
${exe} admin auth add-oauth ${oauthConfigArgs}
|
||||||
|
fi
|
||||||
|
'')
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = {
|
||||||
|
extraConfig = lib.mkAfter ''
|
||||||
|
rewrite ^/user/login$ /user/oauth2/${oauth2-provider-name} last;
|
||||||
|
# FIXME is it needed?
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
selfprivacy.auth.clients."${oauthClientID}" = {
|
||||||
|
inherit adminsGroup usersGroup;
|
||||||
|
imageFile = ./icon.svg;
|
||||||
|
displayName = "Forgejo";
|
||||||
|
subdomain = cfg.subdomain;
|
||||||
|
isTokenNeeded = true;
|
||||||
|
originLanding = "https://${cfg.subdomain}.${sp.domain}/user/login?redirect_to=%2f";
|
||||||
|
originUrl = redirect-uri;
|
||||||
|
clientSystemdUnits = [ "forgejo.service" ];
|
||||||
|
enablePkce = lib.versionAtLeast forgejoPackage.version "8.0";
|
||||||
|
linuxUserOfClient = linuxUserOfService;
|
||||||
|
linuxGroupOfClient = linuxGroupOfService;
|
||||||
|
claimMaps.groups = {
|
||||||
|
joinType = "array";
|
||||||
|
valuesByGroup.${adminsGroup} = [ "admins" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
})
|
||||||
systemd = {
|
]
|
||||||
services.forgejo = {
|
);
|
||||||
unitConfig.RequiresMountsFor = lib.mkIf sp.useBinds "/volumes/${cfg.location}/gitea";
|
|
||||||
serviceConfig = {
|
|
||||||
Slice = "gitea.slice";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
slices.gitea = {
|
|
||||||
description = "Forgejo service slice";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
# the following part is active only when enableSso = true
|
|
||||||
(lib.mkIf is-auth-enabled {
|
|
||||||
services.forgejo.settings = {
|
|
||||||
auth.DISABLE_LOGIN_FORM = true;
|
|
||||||
service = {
|
|
||||||
DISABLE_REGISTRATION = cfg.disableRegistration;
|
|
||||||
REQUIRE_SIGNIN_VIEW = cfg.requireSigninView;
|
|
||||||
ALLOW_ONLY_EXTERNAL_REGISTRATION = true;
|
|
||||||
SHOW_REGISTRATION_BUTTON = false;
|
|
||||||
ENABLE_BASIC_AUTHENTICATION = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
# disallow explore page and access to private repositories, but allow public
|
|
||||||
"service.explore".REQUIRE_SIGNIN_VIEW = true;
|
|
||||||
|
|
||||||
# TODO control via selfprivacy parameter
|
|
||||||
# "service.explore".DISABLE_USERS_PAGE = true;
|
|
||||||
|
|
||||||
oauth2_client = {
|
|
||||||
REDIRECT_URI = redirect-uri;
|
|
||||||
ACCOUNT_LINKING = "auto";
|
|
||||||
ENABLE_AUTO_REGISTRATION = true;
|
|
||||||
OPENID_CONNECT_SCOPES = "email openid profile";
|
|
||||||
};
|
|
||||||
# doesn't work if LDAP auth source is not active!
|
|
||||||
"cron.sync_external_users" = {
|
|
||||||
ENABLED = true;
|
|
||||||
RUN_AT_START = true;
|
|
||||||
NOTICE_ON_SUCCESS = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
systemd.services.forgejo = {
|
|
||||||
preStart =
|
|
||||||
let
|
|
||||||
waitForURL = url: maxRetries: delaySec: ''
|
|
||||||
for ((i=1; i<=${toString maxRetries}; i++))
|
|
||||||
do
|
|
||||||
if ${lib.getExe pkgs.curl} -X GET --silent --fail "${url}" > /dev/null
|
|
||||||
then
|
|
||||||
echo "${url} responds to GET HTTP request (attempt #$i)"
|
|
||||||
break
|
|
||||||
else
|
|
||||||
echo "${url} does not respond to GET HTTP request (attempt #$i)"
|
|
||||||
echo sleeping for ${toString delaySec} seconds
|
|
||||||
fi
|
|
||||||
sleep ${toString delaySec}
|
|
||||||
done
|
|
||||||
if [[ "$i" -gt "${toString maxRetries}" ]]
|
|
||||||
then
|
|
||||||
echo "error, max attempts to access "${url}" have been used unsuccessfully!"
|
|
||||||
exit 124
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
|
|
||||||
exe = lib.getExe config.services.forgejo.package;
|
|
||||||
# FIXME skip-tls-verify, bind-password
|
|
||||||
ldapConfigArgs = ''
|
|
||||||
--name LDAP \
|
|
||||||
--active \
|
|
||||||
--security-protocol LDAPS \
|
|
||||||
--skip-tls-verify \
|
|
||||||
--host '${auth-passthru.ldap-host}' \
|
|
||||||
--port '${toString auth-passthru.ldap-port}' \
|
|
||||||
--user-search-base '${auth-passthru.ldap-base-dn}' \
|
|
||||||
--user-filter '(&(class=person)(memberof=${usersGroup})(name=%s))' \
|
|
||||||
--admin-filter '(&(class=person)(memberof=${adminsGroup})' \
|
|
||||||
--username-attribute name \
|
|
||||||
--firstname-attribute name \
|
|
||||||
--surname-attribute displayname \
|
|
||||||
--email-attribute mail \
|
|
||||||
--public-ssh-key-attribute sshPublicKey \
|
|
||||||
--bind-dn 'dn=token' \
|
|
||||||
--bind-password "$(< ${serviceAccountTokenFP})" \
|
|
||||||
--synchronize-users
|
|
||||||
'';
|
|
||||||
oauthConfigArgs = ''
|
|
||||||
--name "${oauth2-provider-name}" \
|
|
||||||
--provider openidConnect \
|
|
||||||
--key forgejo \
|
|
||||||
--secret "$(< ${oauthClientSecretFP})" \
|
|
||||||
--group-claim-name groups \
|
|
||||||
--admin-group admins \
|
|
||||||
--auto-discover-url '${oauthDiscoveryURL}'
|
|
||||||
'';
|
|
||||||
in
|
|
||||||
lib.mkMerge [
|
|
||||||
(waitForURL oauthDiscoveryURL 10 10)
|
|
||||||
(lib.mkAfter ''
|
|
||||||
set -o xtrace
|
|
||||||
|
|
||||||
# Check if LDAP is already configured
|
|
||||||
ldap_line="$(${exe} admin auth list | grep LDAP | head -n 1)"
|
|
||||||
|
|
||||||
if [[ -n "$ldap_line" ]]; then
|
|
||||||
# update ldap config
|
|
||||||
id="$(echo "$ldap_line" | ${pkgs.gawk}/bin/awk '{print $1}')"
|
|
||||||
${exe} admin auth update-ldap --id "$id" ${ldapConfigArgs}
|
|
||||||
else
|
|
||||||
# initially configure ldap
|
|
||||||
${exe} admin auth add-ldap ${ldapConfigArgs}
|
|
||||||
fi
|
|
||||||
|
|
||||||
oauth_line="$(${exe} admin auth list | grep "${oauth2-provider-name}" | head -n 1)"
|
|
||||||
if [[ -n "$oauth_line" ]]; then
|
|
||||||
id="$(echo "$oauth_line" | ${pkgs.gawk}/bin/awk '{print $1}')"
|
|
||||||
${exe} admin auth update-oauth --id "$id" ${oauthConfigArgs}
|
|
||||||
else
|
|
||||||
${exe} admin auth add-oauth ${oauthConfigArgs}
|
|
||||||
fi
|
|
||||||
'')
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = {
|
|
||||||
extraConfig = lib.mkAfter ''
|
|
||||||
rewrite ^/user/login$ /user/oauth2/${oauth2-provider-name} last;
|
|
||||||
# FIXME is it needed?
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
selfprivacy.auth.clients."${oauthClientID}" = {
|
|
||||||
inherit adminsGroup usersGroup;
|
|
||||||
imageFile = ./icon.svg;
|
|
||||||
displayName = "Forgejo";
|
|
||||||
subdomain = cfg.subdomain;
|
|
||||||
isTokenNeeded = true;
|
|
||||||
originLanding =
|
|
||||||
"https://${cfg.subdomain}.${sp.domain}/user/login?redirect_to=%2f";
|
|
||||||
originUrl = redirect-uri;
|
|
||||||
clientSystemdUnits = [ "forgejo.service" ];
|
|
||||||
enablePkce = lib.versionAtLeast forgejoPackage.version "8.0";
|
|
||||||
linuxUserOfClient = linuxUserOfService;
|
|
||||||
linuxGroupOfClient = linuxGroupOfService;
|
|
||||||
claimMaps.groups = {
|
|
||||||
joinType = "array";
|
|
||||||
valuesByGroup.${adminsGroup} = [ "admins" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
@@ -1,33 +1,36 @@
|
|||||||
{
|
{
|
||||||
description = "PoC SP module for Jitsi Meet video conferences server";
|
description = "PoC SP module for Jitsi Meet video conferences server";
|
||||||
|
|
||||||
outputs = { self }: {
|
outputs =
|
||||||
nixosModules.default = import ./module.nix;
|
{ self }:
|
||||||
configPathsNeeded =
|
{
|
||||||
builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
|
nixosModules.default = import ./module.nix;
|
||||||
meta = { lib, ... }: {
|
configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
|
||||||
spModuleSchemaVersion = 1;
|
meta =
|
||||||
id = "jitsi-meet";
|
{ lib, ... }:
|
||||||
name = "JitsiMeet";
|
{
|
||||||
description = "Jitsi Meet is a free and open-source video conferencing solution.";
|
spModuleSchemaVersion = 1;
|
||||||
svgIcon = builtins.readFile ./icon.svg;
|
id = "jitsi-meet";
|
||||||
isMovable = false;
|
name = "JitsiMeet";
|
||||||
isRequired = false;
|
description = "Jitsi Meet is a free and open-source video conferencing solution.";
|
||||||
backupDescription = "Secrets that are used to encrypt the communication.";
|
svgIcon = builtins.readFile ./icon.svg;
|
||||||
systemdServices = [
|
isMovable = false;
|
||||||
"prosody.service"
|
isRequired = false;
|
||||||
"jitsi-videobridge2.service"
|
backupDescription = "Secrets that are used to encrypt the communication.";
|
||||||
"jicofo.service"
|
systemdServices = [
|
||||||
];
|
"prosody.service"
|
||||||
folders = [
|
"jitsi-videobridge2.service"
|
||||||
"/var/lib/jitsi-meet"
|
"jicofo.service"
|
||||||
];
|
];
|
||||||
license = [
|
folders = [
|
||||||
lib.licenses.asl20
|
"/var/lib/jitsi-meet"
|
||||||
];
|
];
|
||||||
homepage = "https://jitsi.org/meet";
|
license = [
|
||||||
sourcePage = "https://github.com/jitsi/jitsi-meet";
|
lib.licenses.asl20
|
||||||
supportLevel = "normal";
|
];
|
||||||
|
homepage = "https://jitsi.org/meet";
|
||||||
|
sourcePage = "https://github.com/jitsi/jitsi-meet";
|
||||||
|
supportLevel = "normal";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@@ -5,37 +5,43 @@ let
|
|||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.selfprivacy.modules.jitsi-meet = {
|
options.selfprivacy.modules.jitsi-meet = {
|
||||||
enable = (lib.mkOption {
|
enable =
|
||||||
default = false;
|
(lib.mkOption {
|
||||||
type = lib.types.bool;
|
default = false;
|
||||||
description = "Enable JitsiMeet";
|
type = lib.types.bool;
|
||||||
}) // {
|
description = "Enable JitsiMeet";
|
||||||
meta = {
|
})
|
||||||
type = "enable";
|
// {
|
||||||
|
meta = {
|
||||||
|
type = "enable";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
subdomain =
|
||||||
subdomain = (lib.mkOption {
|
(lib.mkOption {
|
||||||
default = "meet";
|
default = "meet";
|
||||||
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
||||||
description = "Subdomain";
|
description = "Subdomain";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
widget = "subdomain";
|
meta = {
|
||||||
type = "string";
|
widget = "subdomain";
|
||||||
regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
type = "string";
|
||||||
weight = 0;
|
regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
||||||
|
weight = 0;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
appName =
|
||||||
appName = (lib.mkOption {
|
(lib.mkOption {
|
||||||
default = "Jitsi Meet";
|
default = "Jitsi Meet";
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "The name displayed in the web interface";
|
description = "The name displayed in the web interface";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "string";
|
meta = {
|
||||||
weight = 1;
|
type = "string";
|
||||||
|
weight = 1;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
@@ -43,7 +49,9 @@ in
|
|||||||
(_: prev: {
|
(_: prev: {
|
||||||
# We disable E2E for clients below
|
# We disable E2E for clients below
|
||||||
jitsi-meet = prev.jitsi-meet.overrideAttrs (old: {
|
jitsi-meet = prev.jitsi-meet.overrideAttrs (old: {
|
||||||
meta = old.meta // { knownVulnerabilities = [ ]; };
|
meta = old.meta // {
|
||||||
|
knownVulnerabilities = [ ];
|
||||||
|
};
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
@@ -1,36 +1,39 @@
|
|||||||
{
|
{
|
||||||
description = "PoC SP module for Prometheus-based monitoring";
|
description = "PoC SP module for Prometheus-based monitoring";
|
||||||
|
|
||||||
outputs = { self }: {
|
outputs =
|
||||||
nixosModules.default = import ./module.nix;
|
{ self }:
|
||||||
configPathsNeeded =
|
{
|
||||||
builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
|
nixosModules.default = import ./module.nix;
|
||||||
meta = { lib, ... }: {
|
configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
|
||||||
spModuleSchemaVersion = 1;
|
meta =
|
||||||
id = "monitoring";
|
{ lib, ... }:
|
||||||
name = "Prometheus";
|
|
||||||
description = "Prometheus is used for resource monitoring and alerts.";
|
|
||||||
svgIcon = builtins.readFile ./icon.svg;
|
|
||||||
isMovable = false;
|
|
||||||
isRequired = true;
|
|
||||||
canBeBackedUp = false;
|
|
||||||
backupDescription = "Backups are not available for Prometheus.";
|
|
||||||
systemdServices = [
|
|
||||||
"prometheus.service"
|
|
||||||
];
|
|
||||||
ownedFolders = [
|
|
||||||
{
|
{
|
||||||
path = "/var/lib/prometheus";
|
spModuleSchemaVersion = 1;
|
||||||
owner = "prometheus";
|
id = "monitoring";
|
||||||
group = "prometheus";
|
name = "Prometheus";
|
||||||
}
|
description = "Prometheus is used for resource monitoring and alerts.";
|
||||||
];
|
svgIcon = builtins.readFile ./icon.svg;
|
||||||
license = [
|
isMovable = false;
|
||||||
lib.licenses.asl20
|
isRequired = true;
|
||||||
];
|
canBeBackedUp = false;
|
||||||
homepage = "https://prometheus.io/";
|
backupDescription = "Backups are not available for Prometheus.";
|
||||||
sourcePage = "https://prometheus.io/";
|
systemdServices = [
|
||||||
supportLevel = "normal";
|
"prometheus.service"
|
||||||
|
];
|
||||||
|
ownedFolders = [
|
||||||
|
{
|
||||||
|
path = "/var/lib/prometheus";
|
||||||
|
owner = "prometheus";
|
||||||
|
group = "prometheus";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
license = [
|
||||||
|
lib.licenses.asl20
|
||||||
|
];
|
||||||
|
homepage = "https://prometheus.io/";
|
||||||
|
sourcePage = "https://prometheus.io/";
|
||||||
|
supportLevel = "normal";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@@ -4,23 +4,27 @@ let
|
|||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.selfprivacy.modules.monitoring = {
|
options.selfprivacy.modules.monitoring = {
|
||||||
enable = (lib.mkOption {
|
enable =
|
||||||
default = false;
|
(lib.mkOption {
|
||||||
type = lib.types.bool;
|
default = false;
|
||||||
description = "Enable monitoring service";
|
type = lib.types.bool;
|
||||||
}) // {
|
description = "Enable monitoring service";
|
||||||
meta = {
|
})
|
||||||
type = "enable";
|
// {
|
||||||
|
meta = {
|
||||||
|
type = "enable";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
location =
|
||||||
location = (lib.mkOption {
|
(lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Monitoring data location";
|
description = "Monitoring data location";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "location";
|
meta = {
|
||||||
|
type = "location";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
};
|
};
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
fileSystems = lib.mkIf config.selfprivacy.useBinds {
|
fileSystems = lib.mkIf config.selfprivacy.useBinds {
|
||||||
@@ -54,15 +58,19 @@ in
|
|||||||
scrapeConfigs = [
|
scrapeConfigs = [
|
||||||
{
|
{
|
||||||
job_name = "node-exporter";
|
job_name = "node-exporter";
|
||||||
static_configs = [{
|
static_configs = [
|
||||||
targets = [ "127.0.0.1:9002" ];
|
{
|
||||||
}];
|
targets = [ "127.0.0.1:9002" ];
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
job_name = "cadvisor";
|
job_name = "cadvisor";
|
||||||
static_configs = [{
|
static_configs = [
|
||||||
targets = [ "127.0.0.1:9003" ];
|
{
|
||||||
}];
|
targets = [ "127.0.0.1:9003" ];
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
@@ -1,35 +1,38 @@
|
|||||||
{
|
{
|
||||||
description = "PoC SP module for Mumble conferences server";
|
description = "PoC SP module for Mumble conferences server";
|
||||||
|
|
||||||
outputs = { self }: {
|
outputs =
|
||||||
nixosModules.default = import ./module.nix;
|
{ self }:
|
||||||
configPathsNeeded =
|
{
|
||||||
builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
|
nixosModules.default = import ./module.nix;
|
||||||
meta = { lib, ... }: {
|
configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
|
||||||
spModuleSchemaVersion = 1;
|
meta =
|
||||||
id = "mumble";
|
{ lib, ... }:
|
||||||
name = "Mumble";
|
{
|
||||||
description = "Open Source, Low Latency, High Quality Voice Chat.";
|
spModuleSchemaVersion = 1;
|
||||||
svgIcon = builtins.readFile ./icon.svg;
|
id = "mumble";
|
||||||
showUrl = false;
|
name = "Mumble";
|
||||||
isMovable = true;
|
description = "Open Source, Low Latency, High Quality Voice Chat.";
|
||||||
isRequired = false;
|
svgIcon = builtins.readFile ./icon.svg;
|
||||||
canBeBackedUp = true;
|
showUrl = false;
|
||||||
backupDescription = "Mumble server data.";
|
isMovable = true;
|
||||||
systemdServices = [
|
isRequired = false;
|
||||||
"murmur.service"
|
canBeBackedUp = true;
|
||||||
];
|
backupDescription = "Mumble server data.";
|
||||||
user = "murmur";
|
systemdServices = [
|
||||||
group = "murmur";
|
"murmur.service"
|
||||||
folders = [
|
];
|
||||||
"/var/lib/murmur"
|
user = "murmur";
|
||||||
];
|
group = "murmur";
|
||||||
license = [
|
folders = [
|
||||||
lib.licenses.bsd3
|
"/var/lib/murmur"
|
||||||
];
|
];
|
||||||
homepage = "https://www.mumble.info";
|
license = [
|
||||||
sourcePage = "https://github.com/mumble-voip/mumble";
|
lib.licenses.bsd3
|
||||||
supportLevel = "normal";
|
];
|
||||||
|
homepage = "https://www.mumble.info";
|
||||||
|
sourcePage = "https://github.com/mumble-voip/mumble";
|
||||||
|
supportLevel = "normal";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,9 @@
|
|||||||
{ config, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
domain = config.selfprivacy.domain;
|
domain = config.selfprivacy.domain;
|
||||||
sp = config.selfprivacy;
|
sp = config.selfprivacy;
|
||||||
@@ -6,55 +11,65 @@ let
|
|||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.selfprivacy.modules.mumble = {
|
options.selfprivacy.modules.mumble = {
|
||||||
enable = (lib.mkOption {
|
enable =
|
||||||
default = false;
|
(lib.mkOption {
|
||||||
type = lib.types.bool;
|
default = false;
|
||||||
description = "Enable Mumble";
|
type = lib.types.bool;
|
||||||
}) // {
|
description = "Enable Mumble";
|
||||||
meta = {
|
})
|
||||||
type = "enable";
|
// {
|
||||||
|
meta = {
|
||||||
|
type = "enable";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
subdomain =
|
||||||
subdomain = (lib.mkOption {
|
(lib.mkOption {
|
||||||
default = "mumble";
|
default = "mumble";
|
||||||
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
||||||
description = "Subdomain";
|
description = "Subdomain";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
widget = "subdomain";
|
meta = {
|
||||||
type = "string";
|
widget = "subdomain";
|
||||||
regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
type = "string";
|
||||||
weight = 0;
|
regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
||||||
|
weight = 0;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
location =
|
||||||
location = (lib.mkOption {
|
(lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Location";
|
description = "Location";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "location";
|
meta = {
|
||||||
|
type = "location";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
appName =
|
||||||
appName = (lib.mkOption {
|
(lib.mkOption {
|
||||||
default = "SelfPrivacy Mumble Service";
|
default = "SelfPrivacy Mumble Service";
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "The name of your Mumble server";
|
description = "The name of your Mumble server";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "string";
|
meta = {
|
||||||
weight = 1;
|
type = "string";
|
||||||
|
weight = 1;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
welcomeText =
|
||||||
welcomeText = (lib.mkOption {
|
(lib.mkOption {
|
||||||
default = "Welcome to my Mumble server!";
|
default = "Welcome to my Mumble server!";
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Welcome message";
|
description = "Welcome message";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "string";
|
meta = {
|
||||||
weight = 2;
|
type = "string";
|
||||||
|
weight = 2;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
@@ -79,7 +94,9 @@ in
|
|||||||
};
|
};
|
||||||
systemd = {
|
systemd = {
|
||||||
services = {
|
services = {
|
||||||
murmur = { serviceConfig.Slice = "mumble.slice"; };
|
murmur = {
|
||||||
|
serviceConfig.Slice = "mumble.slice";
|
||||||
|
};
|
||||||
murmur-ensure-folder-ownership = {
|
murmur-ensure-folder-ownership = {
|
||||||
description = "Ensure murmur folder ownership";
|
description = "Ensure murmur folder ownership";
|
||||||
before = [ "murmur.service" ];
|
before = [ "murmur.service" ];
|
||||||
|
@@ -13,8 +13,8 @@ in
|
|||||||
system.activationScripts.nextcloudSecrets =
|
system.activationScripts.nextcloudSecrets =
|
||||||
lib.trivial.warn
|
lib.trivial.warn
|
||||||
(
|
(
|
||||||
"nextcloud service is disabled, " +
|
"nextcloud service is disabled, "
|
||||||
"${override-config-fp}, ${db-pass-filepath} and ${admin-pass-filepath} will be removed!"
|
+ "${override-config-fp}, ${db-pass-filepath} and ${admin-pass-filepath} will be removed!"
|
||||||
)
|
)
|
||||||
''
|
''
|
||||||
rm -f -v ${db-pass-filepath}
|
rm -f -v ${db-pass-filepath}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
config: rec {
|
config: rec {
|
||||||
sp = config.selfprivacy;
|
sp = config.selfprivacy;
|
||||||
domain= sp.domain;
|
domain = sp.domain;
|
||||||
secrets-filepath = "/etc/selfprivacy/secrets.json";
|
secrets-filepath = "/etc/selfprivacy/secrets.json";
|
||||||
db-pass-filepath = "/var/lib/nextcloud/db-pass";
|
db-pass-filepath = "/var/lib/nextcloud/db-pass";
|
||||||
admin-pass-filepath = "/var/lib/nextcloud/admin-pass";
|
admin-pass-filepath = "/var/lib/nextcloud/admin-pass";
|
||||||
|
@@ -1,38 +1,45 @@
|
|||||||
{
|
{
|
||||||
description = "PoC SP module for nextcloud";
|
description = "PoC SP module for nextcloud";
|
||||||
|
|
||||||
outputs = { self }: {
|
outputs =
|
||||||
nixosModules.default = _:
|
{ self }:
|
||||||
{ imports = [ ./module.nix ./cleanup-module.nix ]; };
|
{
|
||||||
configPathsNeeded =
|
nixosModules.default = _: {
|
||||||
builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
|
imports = [
|
||||||
meta = { lib, ... }: {
|
./module.nix
|
||||||
spModuleSchemaVersion = 1;
|
./cleanup-module.nix
|
||||||
id = "nextcloud";
|
];
|
||||||
name = "Nextcloud";
|
|
||||||
description = "Nextcloud is a cloud storage service that offers a web interface and a desktop client.";
|
|
||||||
svgIcon = builtins.readFile ./icon.svg;
|
|
||||||
isMovable = true;
|
|
||||||
isRequired = false;
|
|
||||||
canBeBackedUp = true;
|
|
||||||
backupDescription = "All the files and other data stored in Nextcloud.";
|
|
||||||
systemdServices = [
|
|
||||||
"phpfpm-nextcloud.service"
|
|
||||||
"redis-nextcloud.service"
|
|
||||||
];
|
|
||||||
folders = [
|
|
||||||
"/var/lib/nextcloud"
|
|
||||||
];
|
|
||||||
license = [
|
|
||||||
lib.licenses.agpl3Plus
|
|
||||||
];
|
|
||||||
homepage = "https://nextcloud.com/";
|
|
||||||
sourcePage = "https://github.com/nextcloud";
|
|
||||||
supportLevel = "normal";
|
|
||||||
sso = {
|
|
||||||
userGroup = "sp.nextcloud.users";
|
|
||||||
adminGroup = "sp.nextcloud.admins";
|
|
||||||
};
|
};
|
||||||
|
configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
|
||||||
|
meta =
|
||||||
|
{ lib, ... }:
|
||||||
|
{
|
||||||
|
spModuleSchemaVersion = 1;
|
||||||
|
id = "nextcloud";
|
||||||
|
name = "Nextcloud";
|
||||||
|
description = "Nextcloud is a cloud storage service that offers a web interface and a desktop client.";
|
||||||
|
svgIcon = builtins.readFile ./icon.svg;
|
||||||
|
isMovable = true;
|
||||||
|
isRequired = false;
|
||||||
|
canBeBackedUp = true;
|
||||||
|
backupDescription = "All the files and other data stored in Nextcloud.";
|
||||||
|
systemdServices = [
|
||||||
|
"phpfpm-nextcloud.service"
|
||||||
|
"redis-nextcloud.service"
|
||||||
|
];
|
||||||
|
folders = [
|
||||||
|
"/var/lib/nextcloud"
|
||||||
|
];
|
||||||
|
license = [
|
||||||
|
lib.licenses.agpl3Plus
|
||||||
|
];
|
||||||
|
homepage = "https://nextcloud.com/";
|
||||||
|
sourcePage = "https://github.com/nextcloud";
|
||||||
|
supportLevel = "normal";
|
||||||
|
sso = {
|
||||||
|
userGroup = "sp.nextcloud.users";
|
||||||
|
adminGroup = "sp.nextcloud.admins";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,9 @@
|
|||||||
{ config, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
inherit (import ./common.nix config)
|
inherit (import ./common.nix config)
|
||||||
admin-pass-filepath
|
admin-pass-filepath
|
||||||
@@ -27,391 +32,417 @@ let
|
|||||||
usersGroup = "sp.${oauthClientID}.users";
|
usersGroup = "sp.${oauthClientID}.users";
|
||||||
wildcardGroup = "sp.${oauthClientID}.*";
|
wildcardGroup = "sp.${oauthClientID}.*";
|
||||||
|
|
||||||
serviceAccountTokenFP =
|
serviceAccountTokenFP = auth-passthru.mkServiceAccountTokenFP linuxUserOfService;
|
||||||
auth-passthru.mkServiceAccountTokenFP linuxUserOfService;
|
oauthClientSecretFP = auth-passthru.mkOAuth2ClientSecretFP linuxUserOfService;
|
||||||
oauthClientSecretFP =
|
|
||||||
auth-passthru.mkOAuth2ClientSecretFP linuxUserOfService;
|
|
||||||
|
|
||||||
updater-page-substitute =
|
updater-page-substitute = pkgs.runCommandNoCC "nextcloud-updater-page-substitute" { } ''
|
||||||
pkgs.runCommandNoCC "nextcloud-updater-page-substitute" { } ''
|
install -m644 ${./updater.html} -DT $out/index.html
|
||||||
install -m644 ${./updater.html} -DT $out/index.html
|
'';
|
||||||
'';
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.selfprivacy.modules.nextcloud = with lib; {
|
options.selfprivacy.modules.nextcloud = with lib; {
|
||||||
enable = (lib.mkOption {
|
enable =
|
||||||
default = false;
|
(lib.mkOption {
|
||||||
type = lib.types.bool;
|
default = false;
|
||||||
description = "Enable Nextcloud";
|
type = lib.types.bool;
|
||||||
}) // {
|
description = "Enable Nextcloud";
|
||||||
meta = {
|
})
|
||||||
type = "enable";
|
// {
|
||||||
|
meta = {
|
||||||
|
type = "enable";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
location =
|
||||||
location = (lib.mkOption {
|
(lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Nextcloud location";
|
description = "Nextcloud location";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "location";
|
meta = {
|
||||||
|
type = "location";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
subdomain =
|
||||||
subdomain = (lib.mkOption {
|
(lib.mkOption {
|
||||||
default = "cloud";
|
default = "cloud";
|
||||||
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
||||||
description = "Subdomain";
|
description = "Subdomain";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
widget = "subdomain";
|
meta = {
|
||||||
type = "string";
|
widget = "subdomain";
|
||||||
regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
type = "string";
|
||||||
weight = 0;
|
regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
||||||
|
weight = 0;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
enableImagemagick =
|
||||||
enableImagemagick = (lib.mkOption {
|
(lib.mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
description = "Enable ImageMagick";
|
description = "Enable ImageMagick";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "bool";
|
meta = {
|
||||||
weight = 1;
|
type = "bool";
|
||||||
|
weight = 1;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
enableSso =
|
||||||
enableSso = (lib.mkOption {
|
(lib.mkOption {
|
||||||
default = false;
|
default = false;
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
description = "Enable Single Sign-On";
|
description = "Enable Single Sign-On";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "bool";
|
meta = {
|
||||||
weight = 2;
|
type = "bool";
|
||||||
|
weight = 2;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
enableSambaFeatures =
|
||||||
enableSambaFeatures = (lib.mkOption {
|
(lib.mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = false;
|
default = false;
|
||||||
description = "Enable support for Samba/CIFS features";
|
description = "Enable support for Samba/CIFS features";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "bool";
|
meta = {
|
||||||
weight = 3;
|
type = "bool";
|
||||||
|
weight = 3;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
debug =
|
||||||
debug = (lib.mkOption {
|
(lib.mkOption {
|
||||||
default = false;
|
default = false;
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
description = "Enable debug logging";
|
description = "Enable debug logging";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "bool";
|
meta = {
|
||||||
weight = 4;
|
type = "bool";
|
||||||
|
weight = 4;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
disableMaintenanceModeAtStart =
|
||||||
disableMaintenanceModeAtStart = (lib.mkOption {
|
(lib.mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = false;
|
default = false;
|
||||||
description = "Disable maintenance mode at Nextcloud service startup";
|
description = "Disable maintenance mode at Nextcloud service startup";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "bool";
|
meta = {
|
||||||
weight = 5;
|
type = "bool";
|
||||||
|
weight = 5;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
# config = lib.mkIf sp.modules.nextcloud.enable
|
# config = lib.mkIf sp.modules.nextcloud.enable
|
||||||
config = lib.mkIf sp.modules.nextcloud.enable (lib.mkMerge [
|
config = lib.mkIf sp.modules.nextcloud.enable (
|
||||||
{
|
lib.mkMerge [
|
||||||
assertions = [
|
{
|
||||||
{
|
assertions = [
|
||||||
assertion = cfg.enableSso -> sp.sso.enable;
|
{
|
||||||
message =
|
assertion = cfg.enableSso -> sp.sso.enable;
|
||||||
"SSO cannot be enabled for Nextcloud when SSO is disabled globally.";
|
message = "SSO cannot be enabled for Nextcloud when SSO is disabled globally.";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
fileSystems = lib.mkIf sp.useBinds {
|
fileSystems = lib.mkIf sp.useBinds {
|
||||||
"/var/lib/nextcloud" = {
|
"/var/lib/nextcloud" = {
|
||||||
device = "/volumes/${cfg.location}/nextcloud";
|
device = "/volumes/${cfg.location}/nextcloud";
|
||||||
options = [
|
options = [
|
||||||
"bind"
|
"bind"
|
||||||
"x-systemd.required-by=nextcloud-setup.service"
|
"x-systemd.required-by=nextcloud-setup.service"
|
||||||
"x-systemd.required-by=nextcloud-secrets.service"
|
"x-systemd.required-by=nextcloud-secrets.service"
|
||||||
"x-systemd.before=nextcloud-setup.service"
|
"x-systemd.before=nextcloud-setup.service"
|
||||||
"x-systemd.before=nextcloud-secrets.service"
|
"x-systemd.before=nextcloud-secrets.service"
|
||||||
];
|
];
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# for ExecStartPost script to have access to /run/keys/*
|
|
||||||
users.groups.keys.members =
|
|
||||||
lib.mkIf is-auth-enabled [ linuxUserOfService ];
|
|
||||||
|
|
||||||
# not needed, due to turnOffCertCheck=1 in used_ldap
|
|
||||||
# users.groups.${config.security.acme.certs.${domain}.group}.members =
|
|
||||||
# [ config.services.phpfpm.pools.nextcloud.user ];
|
|
||||||
|
|
||||||
systemd = {
|
|
||||||
services = {
|
|
||||||
phpfpm-nextcloud.serviceConfig.Slice = lib.mkForce "nextcloud.slice";
|
|
||||||
nextcloud-setup = {
|
|
||||||
serviceConfig.Slice = "nextcloud.slice";
|
|
||||||
serviceConfig.Group = config.services.phpfpm.pools.nextcloud.group;
|
|
||||||
};
|
};
|
||||||
nextcloud-cron.serviceConfig.Slice = "nextcloud.slice";
|
};
|
||||||
nextcloud-update-db.serviceConfig.Slice = "nextcloud.slice";
|
|
||||||
nextcloud-update-plugins.serviceConfig.Slice = "nextcloud.slice";
|
|
||||||
nextcloud-secrets = {
|
|
||||||
before = [ "nextcloud-setup.service" ];
|
|
||||||
requiredBy = [ "nextcloud-setup.service" ];
|
|
||||||
serviceConfig.Type = "oneshot";
|
|
||||||
path = with pkgs; [ coreutils jq ];
|
|
||||||
script = ''
|
|
||||||
databasePassword=$(jq -re '.modules.nextcloud.databasePassword' ${secrets-filepath})
|
|
||||||
adminPassword=$(jq -re '.modules.nextcloud.adminPassword' ${secrets-filepath})
|
|
||||||
|
|
||||||
install -C -m 0440 -o nextcloud -g nextcloud -DT \
|
# for ExecStartPost script to have access to /run/keys/*
|
||||||
<(printf "%s\n" "$databasePassword") \
|
users.groups.keys.members = lib.mkIf is-auth-enabled [ linuxUserOfService ];
|
||||||
${db-pass-filepath}
|
|
||||||
|
|
||||||
install -C -m 0440 -o nextcloud -g nextcloud -DT \
|
# not needed, due to turnOffCertCheck=1 in used_ldap
|
||||||
<(printf "%s\n" "$adminPassword") \
|
# users.groups.${config.security.acme.certs.${domain}.group}.members =
|
||||||
${admin-pass-filepath}
|
# [ config.services.phpfpm.pools.nextcloud.user ];
|
||||||
|
|
||||||
|
systemd = {
|
||||||
|
services = {
|
||||||
|
phpfpm-nextcloud.serviceConfig.Slice = lib.mkForce "nextcloud.slice";
|
||||||
|
nextcloud-setup = {
|
||||||
|
serviceConfig.Slice = "nextcloud.slice";
|
||||||
|
serviceConfig.Group = config.services.phpfpm.pools.nextcloud.group;
|
||||||
|
};
|
||||||
|
nextcloud-cron.serviceConfig.Slice = "nextcloud.slice";
|
||||||
|
nextcloud-update-db.serviceConfig.Slice = "nextcloud.slice";
|
||||||
|
nextcloud-update-plugins.serviceConfig.Slice = "nextcloud.slice";
|
||||||
|
nextcloud-secrets = {
|
||||||
|
before = [ "nextcloud-setup.service" ];
|
||||||
|
requiredBy = [ "nextcloud-setup.service" ];
|
||||||
|
serviceConfig.Type = "oneshot";
|
||||||
|
path = with pkgs; [
|
||||||
|
coreutils
|
||||||
|
jq
|
||||||
|
];
|
||||||
|
script = ''
|
||||||
|
databasePassword=$(jq -re '.modules.nextcloud.databasePassword' ${secrets-filepath})
|
||||||
|
adminPassword=$(jq -re '.modules.nextcloud.adminPassword' ${secrets-filepath})
|
||||||
|
|
||||||
|
install -C -m 0440 -o nextcloud -g nextcloud -DT \
|
||||||
|
<(printf "%s\n" "$databasePassword") \
|
||||||
|
${db-pass-filepath}
|
||||||
|
|
||||||
|
install -C -m 0440 -o nextcloud -g nextcloud -DT \
|
||||||
|
<(printf "%s\n" "$adminPassword") \
|
||||||
|
${admin-pass-filepath}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
slices.nextcloud = {
|
||||||
|
description = "Nextcloud service slice";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
services.nextcloud = {
|
||||||
|
enable = true;
|
||||||
|
package = pkgs.nextcloud30;
|
||||||
|
inherit hostName;
|
||||||
|
|
||||||
|
# Use HTTPS for links
|
||||||
|
https = true;
|
||||||
|
|
||||||
|
# auto-update Nextcloud Apps
|
||||||
|
autoUpdateApps.enable = true;
|
||||||
|
# set what time makes sense for you
|
||||||
|
autoUpdateApps.startAt = "05:00:00";
|
||||||
|
|
||||||
|
phpOptions.display_errors = "Off";
|
||||||
|
phpOptions."opcache.interned_strings_buffer" = "32";
|
||||||
|
|
||||||
|
configureRedis = true;
|
||||||
|
|
||||||
|
settings =
|
||||||
|
{
|
||||||
|
# further forces Nextcloud to use HTTPS
|
||||||
|
overwriteprotocol = "https";
|
||||||
|
}
|
||||||
|
// lib.attrsets.optionalAttrs is-auth-enabled {
|
||||||
|
loglevel = 0;
|
||||||
|
# log_type = "file";
|
||||||
|
social_login_auto_redirect = false;
|
||||||
|
|
||||||
|
allow_local_remote_servers = true;
|
||||||
|
allow_user_to_change_display_name = false;
|
||||||
|
lost_password_link = "disabled";
|
||||||
|
allow_multiple_user_backends = false;
|
||||||
|
|
||||||
|
updatechecker = false; # nixpkgs handles updates for us, update via web ui will fail on nixos.
|
||||||
|
|
||||||
|
user_oidc = {
|
||||||
|
single_logout = true;
|
||||||
|
use_pkce = true;
|
||||||
|
auto_provision = true;
|
||||||
|
soft_auto_provision = true;
|
||||||
|
disable_account_creation = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = {
|
||||||
|
dbtype = "sqlite";
|
||||||
|
dbuser = "nextcloud";
|
||||||
|
dbname = "nextcloud";
|
||||||
|
dbpassFile = db-pass-filepath;
|
||||||
|
# TODO review whether admin user is needed at all - admin group works
|
||||||
|
adminpassFile = admin-pass-filepath;
|
||||||
|
adminuser = "admin";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
services.nginx.virtualHosts.${hostName} = {
|
||||||
|
useACMEHost = sp.domain;
|
||||||
|
forceSSL = true;
|
||||||
|
#locations."/".extraConfig = lib.mkIf is-auth-enabled ''
|
||||||
|
# # FIXME does not work
|
||||||
|
# rewrite ^/login$ /apps/user_oidc/login/1 last;
|
||||||
|
#'';
|
||||||
|
# show an error instead of a blank page on Nextcloud PHP/FastCGI error
|
||||||
|
locations."~ \\.php(?:$|/)".extraConfig = ''
|
||||||
|
error_page 500 502 503 504 ${pkgs.nginx}/html/50x.html;
|
||||||
|
'';
|
||||||
|
locations."^~ /updater/" = {
|
||||||
|
alias = updater-page-substitute + "/";
|
||||||
|
extraConfig = ''
|
||||||
|
error_page 410 /index.html;
|
||||||
|
# otherwise, nginx returns 405 for POST requests to static content
|
||||||
|
error_page 405 =200 $uri;
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
slices.nextcloud = {
|
}
|
||||||
description = "Nextcloud service slice";
|
# enables samba features when requested
|
||||||
|
(lib.mkIf cfg.enableSambaFeatures {
|
||||||
|
# only apply cifs-utils package to this module
|
||||||
|
services.phpfpm.pools.nextcloud.phpEnv.PATH =
|
||||||
|
lib.mkForce "${pkgs.samba}/bin:${pkgs.cifs-utils}/bin:/run/wrappers/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin:/usr/bin:/bin";
|
||||||
|
systemd.services.nextcloud-cron.path = [
|
||||||
|
pkgs.samba
|
||||||
|
pkgs.cifs-utils
|
||||||
|
];
|
||||||
|
})
|
||||||
|
# the following part is active only when "auth" module is enabled
|
||||||
|
(lib.mkIf is-auth-enabled {
|
||||||
|
systemd.services.nextcloud-setup = {
|
||||||
|
serviceConfig = {
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = "60";
|
||||||
|
};
|
||||||
|
path = [ pkgs.jq ];
|
||||||
|
script = lib.mkMerge [
|
||||||
|
(lib.strings.optionalString cfg.disableMaintenanceModeAtStart (
|
||||||
|
lib.mkBefore "${occ} maintenance:mode --no-interaction --off"
|
||||||
|
))
|
||||||
|
''
|
||||||
|
set -o errexit
|
||||||
|
set -o nounset
|
||||||
|
${lib.strings.optionalString cfg.debug "set -o xtrace"}
|
||||||
|
|
||||||
|
${occ} app:disable logreader
|
||||||
|
|
||||||
|
${occ} app:install user_ldap || :
|
||||||
|
${occ} app:enable user_ldap
|
||||||
|
|
||||||
|
# The following code tries to match an existing config or creates a new one.
|
||||||
|
# The criteria for matching is the ldapHost value.
|
||||||
|
|
||||||
|
# remove broken link after previous nextcloud (un)installation
|
||||||
|
[[ ! -f "${override-config-fp}" && -L "${override-config-fp}" ]] && \
|
||||||
|
rm -v "${override-config-fp}"
|
||||||
|
|
||||||
|
ALL_CONFIG="$(${occ} ldap:show-config --output=json)"
|
||||||
|
|
||||||
|
MATCHING_CONFIG_IDs="$(jq '[to_entries[] | select(.value.ldapHost=="${ldap_scheme_and_host}") | .key]' <<<"$ALL_CONFIG")"
|
||||||
|
if [[ $(jq 'length' <<<"$MATCHING_CONFIG_IDs") > 0 ]]; then
|
||||||
|
CONFIG_ID="$(jq --raw-output '.[0]' <<<"$MATCHING_CONFIG_IDs")"
|
||||||
|
else
|
||||||
|
CONFIG_ID="$(${occ} ldap:create-empty-config --only-print-prefix)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Using configId $CONFIG_ID"
|
||||||
|
|
||||||
|
# The following CLI commands follow
|
||||||
|
# https://github.com/lldap/lldap/blob/main/example_configs/nextcloud.md#nextcloud-config--the-cli-way
|
||||||
|
|
||||||
|
# StartTLS is not supported in Kanidm due to security risks, whereas
|
||||||
|
# user_ldap doesn't support SASL. Importing certificate doesn't
|
||||||
|
# help:
|
||||||
|
# ${occ} security:certificates:import "${config.security.acme.certs.${domain}.directory}/cert.pem"
|
||||||
|
${occ} ldap:set-config "$CONFIG_ID" 'turnOffCertCheck' '1'
|
||||||
|
|
||||||
|
${occ} ldap:set-config "$CONFIG_ID" 'ldapHost' '${ldap_scheme_and_host}'
|
||||||
|
${occ} ldap:set-config "$CONFIG_ID" 'ldapPort' '${toString auth-passthru.ldap-port}'
|
||||||
|
${occ} ldap:set-config "$CONFIG_ID" 'ldapAgentName' 'dn=token'
|
||||||
|
${occ} ldap:set-config "$CONFIG_ID" 'ldapAgentPassword' "$(<${serviceAccountTokenFP})"
|
||||||
|
${occ} ldap:set-config "$CONFIG_ID" 'ldapBase' '${auth-passthru.ldap-base-dn}'
|
||||||
|
${occ} ldap:set-config "$CONFIG_ID" 'ldapBaseGroups' '${auth-passthru.ldap-base-dn}'
|
||||||
|
${occ} ldap:set-config "$CONFIG_ID" 'ldapBaseUsers' '${auth-passthru.ldap-base-dn}'
|
||||||
|
${occ} ldap:set-config "$CONFIG_ID" 'ldapEmailAttribute' 'mail'
|
||||||
|
${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilter' \
|
||||||
|
'(&(class=group)(${wildcardGroup})'
|
||||||
|
${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilterGroups' \
|
||||||
|
'(&(class=group)(${wildcardGroup}))'
|
||||||
|
# ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilterObjectclass' \
|
||||||
|
# 'groupOfUniqueNames'
|
||||||
|
# ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupMemberAssocAttr' \
|
||||||
|
# 'uniqueMember'
|
||||||
|
${occ} ldap:set-config "$CONFIG_ID" 'ldapLoginFilter' \
|
||||||
|
'(&(class=person)(memberof=${usersGroup})(uid=%uid))'
|
||||||
|
${occ} ldap:set-config "$CONFIG_ID" 'ldapLoginFilterAttributes' \
|
||||||
|
'uid'
|
||||||
|
${occ} ldap:set-config "$CONFIG_ID" 'ldapUserDisplayName' \
|
||||||
|
'displayname'
|
||||||
|
${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilter' \
|
||||||
|
'(&(class=person)(memberof=${usersGroup})(name=%s))'
|
||||||
|
${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilterMode' \
|
||||||
|
'1'
|
||||||
|
${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilterObjectclass' \
|
||||||
|
'person'
|
||||||
|
|
||||||
|
${occ} ldap:test-config -- "$CONFIG_ID"
|
||||||
|
|
||||||
|
# delete all configs except "$CONFIG_ID"
|
||||||
|
for configid in $(jq --raw-output "keys[] | select(. != \"$CONFIG_ID\")" <<<"$ALL_CONFIG"); do
|
||||||
|
echo "Deactivating $configid"
|
||||||
|
${occ} ldap:set-config "$configid" 'ldapConfigurationActive' '0'
|
||||||
|
echo "Deactivated $configid"
|
||||||
|
echo "Deleting $configid"
|
||||||
|
${occ} ldap:delete-config "$configid"
|
||||||
|
echo "Deleted $configid"
|
||||||
|
done
|
||||||
|
|
||||||
|
${occ} ldap:set-config "$CONFIG_ID" 'ldapConfigurationActive' '1'
|
||||||
|
|
||||||
|
############################################################################
|
||||||
|
# OIDC app
|
||||||
|
############################################################################
|
||||||
|
${occ} app:install user_oidc || :
|
||||||
|
${occ} app:enable user_oidc
|
||||||
|
|
||||||
|
${occ} user_oidc:provider ${auth-passthru.oauth2-provider-name} \
|
||||||
|
--clientid="${oauthClientID}" \
|
||||||
|
--clientsecret="$(<${oauthClientSecretFP})" \
|
||||||
|
--discoveryuri="${auth-passthru.oauth2-discovery-url "nextcloud"}" \
|
||||||
|
--unique-uid=0 \
|
||||||
|
--scope="email openid profile" \
|
||||||
|
--mapping-uid=preferred_username \
|
||||||
|
--no-interaction \
|
||||||
|
--mapping-groups=groups \
|
||||||
|
--group-provisioning=1 \
|
||||||
|
-vvv
|
||||||
|
|
||||||
|
''
|
||||||
|
(lib.optionalString deleteNextcloudAdmin ''
|
||||||
|
if [[ ! -f /var/lib/nextcloud/.admin-user-deleted ]]; then
|
||||||
|
${occ} user:delete admin
|
||||||
|
touch /var/lib/nextcloud/.admin-user-deleted
|
||||||
|
fi
|
||||||
|
'')
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
selfprivacy.auth.clients."${oauthClientID}" = {
|
||||||
services.nextcloud = {
|
inherit adminsGroup usersGroup;
|
||||||
enable = true;
|
imageFile = ./icon.svg;
|
||||||
package = pkgs.nextcloud30;
|
displayName = "Nextcloud";
|
||||||
inherit hostName;
|
subdomain = cfg.subdomain;
|
||||||
|
isTokenNeeded = true;
|
||||||
# Use HTTPS for links
|
originUrl = "https://${cfg.subdomain}.${domain}/apps/user_oidc/code";
|
||||||
https = true;
|
originLanding = "https://${cfg.subdomain}.${domain}/apps/user_oidc/login/1";
|
||||||
|
useShortPreferredUsername = true;
|
||||||
# auto-update Nextcloud Apps
|
clientSystemdUnits = [
|
||||||
autoUpdateApps.enable = true;
|
"nextcloud-setup.service"
|
||||||
# set what time makes sense for you
|
"phpfpm-nextcloud.service"
|
||||||
autoUpdateApps.startAt = "05:00:00";
|
];
|
||||||
|
enablePkce = true;
|
||||||
phpOptions.display_errors = "Off";
|
linuxUserOfClient = linuxUserOfService;
|
||||||
phpOptions."opcache.interned_strings_buffer" = "32";
|
linuxGroupOfClient = linuxGroupOfService;
|
||||||
|
scopeMaps.${usersGroup} = [
|
||||||
configureRedis = true;
|
"email"
|
||||||
|
"openid"
|
||||||
settings = {
|
"profile"
|
||||||
# further forces Nextcloud to use HTTPS
|
];
|
||||||
overwriteprotocol = "https";
|
claimMaps.groups = {
|
||||||
} // lib.attrsets.optionalAttrs is-auth-enabled {
|
joinType = "array";
|
||||||
loglevel = 0;
|
valuesByGroup.${adminsGroup} = [ "admin" ];
|
||||||
# log_type = "file";
|
|
||||||
social_login_auto_redirect = false;
|
|
||||||
|
|
||||||
allow_local_remote_servers = true;
|
|
||||||
allow_user_to_change_display_name = false;
|
|
||||||
lost_password_link = "disabled";
|
|
||||||
allow_multiple_user_backends = false;
|
|
||||||
|
|
||||||
updatechecker = false; # nixpkgs handles updates for us, update via web ui will fail on nixos.
|
|
||||||
|
|
||||||
user_oidc = {
|
|
||||||
single_logout = true;
|
|
||||||
use_pkce = true;
|
|
||||||
auto_provision = true;
|
|
||||||
soft_auto_provision = true;
|
|
||||||
disable_account_creation = false;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
})
|
||||||
config = {
|
(lib.mkIf (!is-auth-enabled) {
|
||||||
dbtype = "sqlite";
|
systemd.services.nextcloud-setup = {
|
||||||
dbuser = "nextcloud";
|
script = ''
|
||||||
dbname = "nextcloud";
|
${occ} app:disable logreader
|
||||||
dbpassFile = db-pass-filepath;
|
${occ} app:disable user_oidc
|
||||||
# TODO review whether admin user is needed at all - admin group works
|
|
||||||
adminpassFile = admin-pass-filepath;
|
|
||||||
adminuser = "admin";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
services.nginx.virtualHosts.${hostName} = {
|
|
||||||
useACMEHost = sp.domain;
|
|
||||||
forceSSL = true;
|
|
||||||
#locations."/".extraConfig = lib.mkIf is-auth-enabled ''
|
|
||||||
# # FIXME does not work
|
|
||||||
# rewrite ^/login$ /apps/user_oidc/login/1 last;
|
|
||||||
#'';
|
|
||||||
# show an error instead of a blank page on Nextcloud PHP/FastCGI error
|
|
||||||
locations."~ \\.php(?:$|/)".extraConfig = ''
|
|
||||||
error_page 500 502 503 504 ${pkgs.nginx}/html/50x.html;
|
|
||||||
'';
|
|
||||||
locations."^~ /updater/" = {
|
|
||||||
alias = updater-page-substitute + "/";
|
|
||||||
extraConfig = ''
|
|
||||||
error_page 410 /index.html;
|
|
||||||
# otherwise, nginx returns 405 for POST requests to static content
|
|
||||||
error_page 405 =200 $uri;
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
})
|
||||||
}
|
]
|
||||||
# enables samba features when requested
|
);
|
||||||
(lib.mkIf cfg.enableSambaFeatures {
|
|
||||||
# only apply cifs-utils package to this module
|
|
||||||
services.phpfpm.pools.nextcloud.phpEnv.PATH =
|
|
||||||
lib.mkForce "${pkgs.samba}/bin:${pkgs.cifs-utils}/bin:/run/wrappers/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin:/usr/bin:/bin";
|
|
||||||
systemd.services.nextcloud-cron.path = [ pkgs.samba pkgs.cifs-utils ];
|
|
||||||
})
|
|
||||||
# the following part is active only when "auth" module is enabled
|
|
||||||
(lib.mkIf is-auth-enabled {
|
|
||||||
systemd.services.nextcloud-setup = {
|
|
||||||
serviceConfig = {
|
|
||||||
Restart = "on-failure";
|
|
||||||
RestartSec = "60";
|
|
||||||
};
|
|
||||||
path = [ pkgs.jq ];
|
|
||||||
script = lib.mkMerge [
|
|
||||||
(lib.strings.optionalString cfg.disableMaintenanceModeAtStart (
|
|
||||||
lib.mkBefore "${occ} maintenance:mode --no-interaction --off"
|
|
||||||
))
|
|
||||||
''
|
|
||||||
set -o errexit
|
|
||||||
set -o nounset
|
|
||||||
${lib.strings.optionalString cfg.debug "set -o xtrace"}
|
|
||||||
|
|
||||||
${occ} app:disable logreader
|
|
||||||
|
|
||||||
${occ} app:install user_ldap || :
|
|
||||||
${occ} app:enable user_ldap
|
|
||||||
|
|
||||||
# The following code tries to match an existing config or creates a new one.
|
|
||||||
# The criteria for matching is the ldapHost value.
|
|
||||||
|
|
||||||
# remove broken link after previous nextcloud (un)installation
|
|
||||||
[[ ! -f "${override-config-fp}" && -L "${override-config-fp}" ]] && \
|
|
||||||
rm -v "${override-config-fp}"
|
|
||||||
|
|
||||||
ALL_CONFIG="$(${occ} ldap:show-config --output=json)"
|
|
||||||
|
|
||||||
MATCHING_CONFIG_IDs="$(jq '[to_entries[] | select(.value.ldapHost=="${ldap_scheme_and_host}") | .key]' <<<"$ALL_CONFIG")"
|
|
||||||
if [[ $(jq 'length' <<<"$MATCHING_CONFIG_IDs") > 0 ]]; then
|
|
||||||
CONFIG_ID="$(jq --raw-output '.[0]' <<<"$MATCHING_CONFIG_IDs")"
|
|
||||||
else
|
|
||||||
CONFIG_ID="$(${occ} ldap:create-empty-config --only-print-prefix)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Using configId $CONFIG_ID"
|
|
||||||
|
|
||||||
# The following CLI commands follow
|
|
||||||
# https://github.com/lldap/lldap/blob/main/example_configs/nextcloud.md#nextcloud-config--the-cli-way
|
|
||||||
|
|
||||||
# StartTLS is not supported in Kanidm due to security risks, whereas
|
|
||||||
# user_ldap doesn't support SASL. Importing certificate doesn't
|
|
||||||
# help:
|
|
||||||
# ${occ} security:certificates:import "${config.security.acme.certs.${domain}.directory}/cert.pem"
|
|
||||||
${occ} ldap:set-config "$CONFIG_ID" 'turnOffCertCheck' '1'
|
|
||||||
|
|
||||||
${occ} ldap:set-config "$CONFIG_ID" 'ldapHost' '${ldap_scheme_and_host}'
|
|
||||||
${occ} ldap:set-config "$CONFIG_ID" 'ldapPort' '${toString auth-passthru.ldap-port}'
|
|
||||||
${occ} ldap:set-config "$CONFIG_ID" 'ldapAgentName' 'dn=token'
|
|
||||||
${occ} ldap:set-config "$CONFIG_ID" 'ldapAgentPassword' "$(<${serviceAccountTokenFP})"
|
|
||||||
${occ} ldap:set-config "$CONFIG_ID" 'ldapBase' '${auth-passthru.ldap-base-dn}'
|
|
||||||
${occ} ldap:set-config "$CONFIG_ID" 'ldapBaseGroups' '${auth-passthru.ldap-base-dn}'
|
|
||||||
${occ} ldap:set-config "$CONFIG_ID" 'ldapBaseUsers' '${auth-passthru.ldap-base-dn}'
|
|
||||||
${occ} ldap:set-config "$CONFIG_ID" 'ldapEmailAttribute' 'mail'
|
|
||||||
${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilter' \
|
|
||||||
'(&(class=group)(${wildcardGroup})'
|
|
||||||
${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilterGroups' \
|
|
||||||
'(&(class=group)(${wildcardGroup}))'
|
|
||||||
# ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilterObjectclass' \
|
|
||||||
# 'groupOfUniqueNames'
|
|
||||||
# ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupMemberAssocAttr' \
|
|
||||||
# 'uniqueMember'
|
|
||||||
${occ} ldap:set-config "$CONFIG_ID" 'ldapLoginFilter' \
|
|
||||||
'(&(class=person)(memberof=${usersGroup})(uid=%uid))'
|
|
||||||
${occ} ldap:set-config "$CONFIG_ID" 'ldapLoginFilterAttributes' \
|
|
||||||
'uid'
|
|
||||||
${occ} ldap:set-config "$CONFIG_ID" 'ldapUserDisplayName' \
|
|
||||||
'displayname'
|
|
||||||
${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilter' \
|
|
||||||
'(&(class=person)(memberof=${usersGroup})(name=%s))'
|
|
||||||
${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilterMode' \
|
|
||||||
'1'
|
|
||||||
${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilterObjectclass' \
|
|
||||||
'person'
|
|
||||||
|
|
||||||
${occ} ldap:test-config -- "$CONFIG_ID"
|
|
||||||
|
|
||||||
# delete all configs except "$CONFIG_ID"
|
|
||||||
for configid in $(jq --raw-output "keys[] | select(. != \"$CONFIG_ID\")" <<<"$ALL_CONFIG"); do
|
|
||||||
echo "Deactivating $configid"
|
|
||||||
${occ} ldap:set-config "$configid" 'ldapConfigurationActive' '0'
|
|
||||||
echo "Deactivated $configid"
|
|
||||||
echo "Deleting $configid"
|
|
||||||
${occ} ldap:delete-config "$configid"
|
|
||||||
echo "Deleted $configid"
|
|
||||||
done
|
|
||||||
|
|
||||||
${occ} ldap:set-config "$CONFIG_ID" 'ldapConfigurationActive' '1'
|
|
||||||
|
|
||||||
############################################################################
|
|
||||||
# OIDC app
|
|
||||||
############################################################################
|
|
||||||
${occ} app:install user_oidc || :
|
|
||||||
${occ} app:enable user_oidc
|
|
||||||
|
|
||||||
${occ} user_oidc:provider ${auth-passthru.oauth2-provider-name} \
|
|
||||||
--clientid="${oauthClientID}" \
|
|
||||||
--clientsecret="$(<${oauthClientSecretFP})" \
|
|
||||||
--discoveryuri="${auth-passthru.oauth2-discovery-url "nextcloud"}" \
|
|
||||||
--unique-uid=0 \
|
|
||||||
--scope="email openid profile" \
|
|
||||||
--mapping-uid=preferred_username \
|
|
||||||
--no-interaction \
|
|
||||||
--mapping-groups=groups \
|
|
||||||
--group-provisioning=1 \
|
|
||||||
-vvv
|
|
||||||
|
|
||||||
''
|
|
||||||
(lib.optionalString deleteNextcloudAdmin ''
|
|
||||||
if [[ ! -f /var/lib/nextcloud/.admin-user-deleted ]]; then
|
|
||||||
${occ} user:delete admin
|
|
||||||
touch /var/lib/nextcloud/.admin-user-deleted
|
|
||||||
fi
|
|
||||||
'')
|
|
||||||
];
|
|
||||||
};
|
|
||||||
selfprivacy.auth.clients."${oauthClientID}" = {
|
|
||||||
inherit adminsGroup usersGroup;
|
|
||||||
imageFile = ./icon.svg;
|
|
||||||
displayName = "Nextcloud";
|
|
||||||
subdomain = cfg.subdomain;
|
|
||||||
isTokenNeeded = true;
|
|
||||||
originUrl = "https://${cfg.subdomain}.${domain}/apps/user_oidc/code";
|
|
||||||
originLanding =
|
|
||||||
"https://${cfg.subdomain}.${domain}/apps/user_oidc/login/1";
|
|
||||||
useShortPreferredUsername = true;
|
|
||||||
clientSystemdUnits =
|
|
||||||
[ "nextcloud-setup.service" "phpfpm-nextcloud.service" ];
|
|
||||||
enablePkce = true;
|
|
||||||
linuxUserOfClient = linuxUserOfService;
|
|
||||||
linuxGroupOfClient = linuxGroupOfService;
|
|
||||||
scopeMaps.${usersGroup} = [ "email" "openid" "profile" ];
|
|
||||||
claimMaps.groups = {
|
|
||||||
joinType = "array";
|
|
||||||
valuesByGroup.${adminsGroup} = [ "admin" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
(lib.mkIf (! is-auth-enabled) {
|
|
||||||
systemd.services.nextcloud-setup = {
|
|
||||||
script = ''
|
|
||||||
${occ} app:disable logreader
|
|
||||||
${occ} app:disable user_oidc
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
@@ -1,29 +1,32 @@
|
|||||||
{
|
{
|
||||||
description = "PoC SP module for OpenConnect VPN server (ocserv)";
|
description = "PoC SP module for OpenConnect VPN server (ocserv)";
|
||||||
|
|
||||||
outputs = { self }: {
|
outputs =
|
||||||
nixosModules.default = import ./module.nix;
|
{ self }:
|
||||||
configPathsNeeded =
|
{
|
||||||
builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
|
nixosModules.default = import ./module.nix;
|
||||||
meta = { lib, ... }: {
|
configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
|
||||||
spModuleSchemaVersion = 1;
|
meta =
|
||||||
id = "ocserv";
|
{ lib, ... }:
|
||||||
name = "OpenConnect VPN";
|
{
|
||||||
description = "OpenConnect VPN to connect your devices and access the internet.";
|
spModuleSchemaVersion = 1;
|
||||||
svgIcon = builtins.readFile ./icon.svg;
|
id = "ocserv";
|
||||||
isMovable = false;
|
name = "OpenConnect VPN";
|
||||||
isRequired = false;
|
description = "OpenConnect VPN to connect your devices and access the internet.";
|
||||||
canBeBackedUp = false;
|
svgIcon = builtins.readFile ./icon.svg;
|
||||||
backupDescription = "Backups are not available for OpenConnect VPN.";
|
isMovable = false;
|
||||||
systemdServices = [
|
isRequired = false;
|
||||||
"ocserv.service"
|
canBeBackedUp = false;
|
||||||
];
|
backupDescription = "Backups are not available for OpenConnect VPN.";
|
||||||
license = [
|
systemdServices = [
|
||||||
lib.licenses.gpl2Plus
|
"ocserv.service"
|
||||||
];
|
];
|
||||||
homepage = "https://gitlab.com/openconnect/ocserv";
|
license = [
|
||||||
sourcePage = "https://gitlab.com/openconnect/ocserv";
|
lib.licenses.gpl2Plus
|
||||||
supportLevel = "deprecated";
|
];
|
||||||
|
homepage = "https://gitlab.com/openconnect/ocserv";
|
||||||
|
sourcePage = "https://gitlab.com/openconnect/ocserv";
|
||||||
|
supportLevel = "deprecated";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@@ -7,15 +7,17 @@ let
|
|||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.selfprivacy.modules.ocserv = {
|
options.selfprivacy.modules.ocserv = {
|
||||||
enable = (lib.mkOption {
|
enable =
|
||||||
default = false;
|
(lib.mkOption {
|
||||||
type = lib.types.bool;
|
default = false;
|
||||||
description = "Enable";
|
type = lib.types.bool;
|
||||||
}) // {
|
description = "Enable";
|
||||||
meta = {
|
})
|
||||||
type = "enable";
|
// {
|
||||||
|
meta = {
|
||||||
|
type = "enable";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
@@ -66,7 +68,10 @@ in
|
|||||||
systemd = {
|
systemd = {
|
||||||
services = {
|
services = {
|
||||||
ocserv = {
|
ocserv = {
|
||||||
unitConfig.ConditionPathExists = [ cert key ];
|
unitConfig.ConditionPathExists = [
|
||||||
|
cert
|
||||||
|
key
|
||||||
|
];
|
||||||
serviceConfig.Slice = "ocserv.slice";
|
serviceConfig.Slice = "ocserv.slice";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -5,13 +5,8 @@ in
|
|||||||
# FIXME do we really want to delete passwords on module deactivation!?
|
# FIXME do we really want to delete passwords on module deactivation!?
|
||||||
{
|
{
|
||||||
config = lib.mkIf (!sp.modules.pleroma.enable) {
|
config = lib.mkIf (!sp.modules.pleroma.enable) {
|
||||||
system.activationScripts.pleroma =
|
system.activationScripts.pleroma = lib.trivial.warn ("pleroma service is disabled, ${secrets-exs} will be removed!") ''
|
||||||
lib.trivial.warn
|
rm -f -v ${secrets-exs}
|
||||||
(
|
'';
|
||||||
"pleroma service is disabled, ${secrets-exs} will be removed!"
|
|
||||||
)
|
|
||||||
''
|
|
||||||
rm -f -v ${secrets-exs}
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
config:
|
config: {
|
||||||
{
|
|
||||||
sp = config.selfprivacy;
|
sp = config.selfprivacy;
|
||||||
secrets-exs = "/var/lib/pleroma/secrets.exs";
|
secrets-exs = "/var/lib/pleroma/secrets.exs";
|
||||||
}
|
}
|
||||||
|
@@ -1,35 +1,38 @@
|
|||||||
{
|
{
|
||||||
description = "PoC SP module for Pleroma lightweight fediverse server";
|
description = "PoC SP module for Pleroma lightweight fediverse server";
|
||||||
|
|
||||||
outputs = { self }: {
|
outputs =
|
||||||
nixosModules.default = import ./module.nix;
|
{ self }:
|
||||||
configPathsNeeded =
|
{
|
||||||
builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
|
nixosModules.default = import ./module.nix;
|
||||||
meta = { lib, ... }: {
|
configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
|
||||||
spModuleSchemaVersion = 1;
|
meta =
|
||||||
id = "pleroma";
|
{ lib, ... }:
|
||||||
name = "Pleroma";
|
{
|
||||||
description = "Pleroma is a microblogging service that offers a web interface and a desktop client.";
|
spModuleSchemaVersion = 1;
|
||||||
svgIcon = builtins.readFile ./icon.svg;
|
id = "pleroma";
|
||||||
isMovable = true;
|
name = "Pleroma";
|
||||||
isRequired = false;
|
description = "Pleroma is a microblogging service that offers a web interface and a desktop client.";
|
||||||
canBeBackedUp = true;
|
svgIcon = builtins.readFile ./icon.svg;
|
||||||
backupDescription = "Your Pleroma accounts, posts and media.";
|
isMovable = true;
|
||||||
systemdServices = [
|
isRequired = false;
|
||||||
"pleroma.service"
|
canBeBackedUp = true;
|
||||||
];
|
backupDescription = "Your Pleroma accounts, posts and media.";
|
||||||
folders = [
|
systemdServices = [
|
||||||
"/var/lib/pleroma"
|
"pleroma.service"
|
||||||
];
|
];
|
||||||
postgreDatabases = [
|
folders = [
|
||||||
"pleroma"
|
"/var/lib/pleroma"
|
||||||
];
|
];
|
||||||
license = [
|
postgreDatabases = [
|
||||||
lib.licenses.agpl3Only
|
"pleroma"
|
||||||
];
|
];
|
||||||
homepage = "https://pleroma.social/";
|
license = [
|
||||||
sourcePage = "https://git.pleroma.social/pleroma/pleroma";
|
lib.licenses.agpl3Only
|
||||||
supportLevel = "deprecated";
|
];
|
||||||
|
homepage = "https://pleroma.social/";
|
||||||
|
sourcePage = "https://git.pleroma.social/pleroma/pleroma";
|
||||||
|
supportLevel = "deprecated";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,9 @@
|
|||||||
{ config, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
secrets-filepath = "/etc/selfprivacy/secrets.json";
|
secrets-filepath = "/etc/selfprivacy/secrets.json";
|
||||||
cfg = config.selfprivacy.modules.pleroma;
|
cfg = config.selfprivacy.modules.pleroma;
|
||||||
@@ -6,35 +11,41 @@ let
|
|||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.selfprivacy.modules.pleroma = {
|
options.selfprivacy.modules.pleroma = {
|
||||||
enable = (lib.mkOption {
|
enable =
|
||||||
default = false;
|
(lib.mkOption {
|
||||||
type = lib.types.bool;
|
default = false;
|
||||||
description = "Enable";
|
type = lib.types.bool;
|
||||||
}) // {
|
description = "Enable";
|
||||||
meta = {
|
})
|
||||||
type = "enable";
|
// {
|
||||||
|
meta = {
|
||||||
|
type = "enable";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
location =
|
||||||
location = (lib.mkOption {
|
(lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Location";
|
description = "Location";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "location";
|
meta = {
|
||||||
|
type = "location";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
subdomain =
|
||||||
subdomain = (lib.mkOption {
|
(lib.mkOption {
|
||||||
default = "social";
|
default = "social";
|
||||||
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
||||||
description = "Subdomain";
|
description = "Subdomain";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
widget = "subdomain";
|
meta = {
|
||||||
type = "string";
|
widget = "subdomain";
|
||||||
regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
type = "string";
|
||||||
weight = 0;
|
regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
||||||
|
weight = 0;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
};
|
};
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
fileSystems = lib.mkIf sp.useBinds {
|
fileSystems = lib.mkIf sp.useBinds {
|
||||||
@@ -55,10 +66,9 @@ in
|
|||||||
user = "pleroma";
|
user = "pleroma";
|
||||||
group = "pleroma";
|
group = "pleroma";
|
||||||
configs = [
|
configs = [
|
||||||
(builtins.replaceStrings
|
(builtins.replaceStrings [ "$DOMAIN" "$LUSER" ] [ sp.domain sp.username ] (
|
||||||
[ "$DOMAIN" "$LUSER" ]
|
builtins.readFile ./config.exs.in
|
||||||
[ sp.domain sp.username ]
|
))
|
||||||
(builtins.readFile ./config.exs.in))
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
postgresql = {
|
postgresql = {
|
||||||
@@ -94,7 +104,10 @@ in
|
|||||||
before = [ "pleroma.service" ];
|
before = [ "pleroma.service" ];
|
||||||
requiredBy = [ "pleroma.service" ];
|
requiredBy = [ "pleroma.service" ];
|
||||||
serviceConfig.Type = "oneshot";
|
serviceConfig.Type = "oneshot";
|
||||||
path = with pkgs; [ coreutils jq ];
|
path = with pkgs; [
|
||||||
|
coreutils
|
||||||
|
jq
|
||||||
|
];
|
||||||
script = ''
|
script = ''
|
||||||
set -o nounset
|
set -o nounset
|
||||||
|
|
||||||
|
@@ -1,36 +1,39 @@
|
|||||||
{
|
{
|
||||||
description = "Roundcube is a web-based email client.";
|
description = "Roundcube is a web-based email client.";
|
||||||
|
|
||||||
outputs = { self }: {
|
outputs =
|
||||||
nixosModules.default = import ./module.nix;
|
{ self }:
|
||||||
configPathsNeeded =
|
{
|
||||||
builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
|
nixosModules.default = import ./module.nix;
|
||||||
meta = { lib, ... }: {
|
configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
|
||||||
spModuleSchemaVersion = 1;
|
meta =
|
||||||
id = "roundcube";
|
{ lib, ... }:
|
||||||
name = "Roundcube";
|
{
|
||||||
description = "Roundcube is an open source webmail software.";
|
spModuleSchemaVersion = 1;
|
||||||
svgIcon = builtins.readFile ./icon.svg;
|
id = "roundcube";
|
||||||
isMovable = false;
|
name = "Roundcube";
|
||||||
isRequired = false;
|
description = "Roundcube is an open source webmail software.";
|
||||||
canBeBackedUp = true;
|
svgIcon = builtins.readFile ./icon.svg;
|
||||||
backupDescription = "Users' settings.";
|
isMovable = false;
|
||||||
postgreDatabases = [
|
isRequired = false;
|
||||||
"roundcube"
|
canBeBackedUp = true;
|
||||||
];
|
backupDescription = "Users' settings.";
|
||||||
systemdServices = [
|
postgreDatabases = [
|
||||||
"phpfpm-roundcube.service"
|
"roundcube"
|
||||||
];
|
];
|
||||||
license = [
|
systemdServices = [
|
||||||
lib.licenses.gpl3
|
"phpfpm-roundcube.service"
|
||||||
];
|
];
|
||||||
homepage = "https://roundcube.net/";
|
license = [
|
||||||
sourcePage = "https://github.com/roundcube/roundcubemail";
|
lib.licenses.gpl3
|
||||||
supportLevel = "normal";
|
];
|
||||||
sso = {
|
homepage = "https://roundcube.net/";
|
||||||
userGroup = "sp.roundcube.users";
|
sourcePage = "https://github.com/roundcube/roundcubemail";
|
||||||
adminGroup = "sp.roundcube.admins";
|
supportLevel = "normal";
|
||||||
};
|
sso = {
|
||||||
|
userGroup = "sp.roundcube.users";
|
||||||
|
adminGroup = "sp.roundcube.admins";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,9 @@
|
|||||||
{ config, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
domain = config.selfprivacy.domain;
|
domain = config.selfprivacy.domain;
|
||||||
cfg = config.selfprivacy.modules.roundcube;
|
cfg = config.selfprivacy.modules.roundcube;
|
||||||
@@ -16,129 +21,136 @@ let
|
|||||||
usersGroup = "sp.${sp-module-name}.users";
|
usersGroup = "sp.${sp-module-name}.users";
|
||||||
|
|
||||||
oauth-donor = config.selfprivacy.passthru.mailserver;
|
oauth-donor = config.selfprivacy.passthru.mailserver;
|
||||||
oauthClientSecretFP =
|
oauthClientSecretFP = auth-passthru.mkOAuth2ClientSecretFP linuxGroupOfService;
|
||||||
auth-passthru.mkOAuth2ClientSecretFP linuxGroupOfService;
|
|
||||||
# copy client secret from mailserver
|
# copy client secret from mailserver
|
||||||
kanidmExecStartPreScriptRoot = pkgs.writeShellScript
|
kanidmExecStartPreScriptRoot = pkgs.writeShellScript "${sp-module-name}-kanidm-ExecStartPre-root-script.sh" ''
|
||||||
"${sp-module-name}-kanidm-ExecStartPre-root-script.sh"
|
install -v -m640 -o kanidm -g ${linuxGroupOfService} ${oauth-donor.oauth-client-secret-fp} ${oauthClientSecretFP}
|
||||||
''
|
'';
|
||||||
install -v -m640 -o kanidm -g ${linuxGroupOfService} ${oauth-donor.oauth-client-secret-fp} ${oauthClientSecretFP}
|
|
||||||
'';
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.selfprivacy.modules.roundcube = {
|
options.selfprivacy.modules.roundcube = {
|
||||||
enable = (lib.mkOption {
|
enable =
|
||||||
default = false;
|
(lib.mkOption {
|
||||||
type = lib.types.bool;
|
default = false;
|
||||||
description = "Enable";
|
type = lib.types.bool;
|
||||||
}) // {
|
description = "Enable";
|
||||||
meta = {
|
})
|
||||||
type = "enable";
|
// {
|
||||||
};
|
meta = {
|
||||||
};
|
type = "enable";
|
||||||
subdomain = (lib.mkOption {
|
|
||||||
default = "roundcube";
|
|
||||||
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
|
||||||
description = "Subdomain";
|
|
||||||
}) // {
|
|
||||||
meta = {
|
|
||||||
widget = "subdomain";
|
|
||||||
type = "string";
|
|
||||||
regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
|
||||||
weight = 0;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
enableSso = (lib.mkOption {
|
|
||||||
default = false;
|
|
||||||
type = lib.types.bool;
|
|
||||||
description = "Enable Single Sign-On";
|
|
||||||
}) // {
|
|
||||||
meta = {
|
|
||||||
type = "bool";
|
|
||||||
weight = 1;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable (lib.mkMerge [
|
|
||||||
{
|
|
||||||
assertions = [
|
|
||||||
{
|
|
||||||
assertion = cfg.enableSso -> config.selfprivacy.sso.enable;
|
|
||||||
message =
|
|
||||||
"SSO cannot be enabled for Roundcube when SSO is disabled globally.";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
services.roundcube = {
|
|
||||||
enable = true;
|
|
||||||
# this is the url of the vhost, not necessarily the same as the fqdn of
|
|
||||||
# the mailserver
|
|
||||||
hostName = "${cfg.subdomain}.${config.selfprivacy.domain}";
|
|
||||||
extraConfig = ''
|
|
||||||
# starttls needed for authentication, so the fqdn required to match
|
|
||||||
# the certificate
|
|
||||||
$config['smtp_host'] = "tls://${config.mailserver.fqdn}";
|
|
||||||
$config['smtp_user'] = "%u";
|
|
||||||
$config['smtp_pass'] = "%p";
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
services.nginx.virtualHosts."${cfg.subdomain}.${domain}" = {
|
|
||||||
forceSSL = true;
|
|
||||||
useACMEHost = domain;
|
|
||||||
enableACME = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.slices.roundcube.description = "Roundcube service slice";
|
|
||||||
# Roundcube depends on Dovecot and its OAuth2 client secret.
|
|
||||||
systemd.services.phpfpm-roundcube.after = [ "dovecot2.service" ];
|
|
||||||
}
|
|
||||||
# the following part is active only when "auth" module is enabled
|
|
||||||
(lib.mkIf is-auth-enabled {
|
|
||||||
services.roundcube.extraConfig = lib.mkAfter ''
|
|
||||||
$config['oauth_provider'] = 'generic';
|
|
||||||
$config['oauth_provider_name'] = '${auth-passthru.oauth2-provider-name}';
|
|
||||||
$config['oauth_client_id'] = '${oauth-donor.oauth-client-id}';
|
|
||||||
$config['oauth_client_secret'] = file_get_contents('${oauthClientSecretFP}');
|
|
||||||
$config['oauth_auth_uri'] = 'https://${auth-fqdn}/ui/oauth2';
|
|
||||||
$config['oauth_token_uri'] = 'https://${auth-fqdn}/oauth2/token';
|
|
||||||
$config['oauth_identity_uri'] = 'https://${auth-fqdn}/oauth2/openid/${oauth-donor.oauth-client-id}/userinfo';
|
|
||||||
$config['oauth_scope'] = 'email profile openid';
|
|
||||||
$config['oauth_auth_parameters'] = [];
|
|
||||||
$config['oauth_identity_fields'] = ['email'];
|
|
||||||
$config['oauth_login_redirect'] = true;
|
|
||||||
$config['auto_create_user'] = true;
|
|
||||||
'';
|
|
||||||
systemd.services.roundcube = {
|
|
||||||
after = [ "dovecot2.service" ];
|
|
||||||
requires = [ "dovecot2.service" ];
|
|
||||||
};
|
|
||||||
systemd.services.kanidm.serviceConfig.ExecStartPre = lib.mkAfter [
|
|
||||||
("-+" + kanidmExecStartPreScriptRoot)
|
|
||||||
];
|
|
||||||
|
|
||||||
selfprivacy.auth.clients."${oauth-donor.oauth-client-id}" = {
|
|
||||||
inherit adminsGroup usersGroup;
|
|
||||||
imageFile = ./icon.svg;
|
|
||||||
displayName = "Roundcube";
|
|
||||||
subdomain = cfg.subdomain;
|
|
||||||
isTokenNeeded = false;
|
|
||||||
isMailserver = true;
|
|
||||||
originUrl = "https://${cfg.subdomain}.${domain}/index.php/login/oauth";
|
|
||||||
originLanding = "https://${cfg.subdomain}.${domain}/";
|
|
||||||
useShortPreferredUsername = false;
|
|
||||||
clientSystemdUnits = [ "dovecot2.service" "phpfpm-roundcube.service" ];
|
|
||||||
enablePkce = false;
|
|
||||||
linuxUserOfClient = linuxUserOfService;
|
|
||||||
linuxGroupOfClient = linuxGroupOfService;
|
|
||||||
scopeMaps = {
|
|
||||||
"${usersGroup}" = [
|
|
||||||
"email"
|
|
||||||
"openid"
|
|
||||||
"profile"
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
})
|
subdomain =
|
||||||
]);
|
(lib.mkOption {
|
||||||
|
default = "roundcube";
|
||||||
|
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
||||||
|
description = "Subdomain";
|
||||||
|
})
|
||||||
|
// {
|
||||||
|
meta = {
|
||||||
|
widget = "subdomain";
|
||||||
|
type = "string";
|
||||||
|
regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
|
||||||
|
weight = 0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
enableSso =
|
||||||
|
(lib.mkOption {
|
||||||
|
default = false;
|
||||||
|
type = lib.types.bool;
|
||||||
|
description = "Enable Single Sign-On";
|
||||||
|
})
|
||||||
|
// {
|
||||||
|
meta = {
|
||||||
|
type = "bool";
|
||||||
|
weight = 1;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable (
|
||||||
|
lib.mkMerge [
|
||||||
|
{
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = cfg.enableSso -> config.selfprivacy.sso.enable;
|
||||||
|
message = "SSO cannot be enabled for Roundcube when SSO is disabled globally.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
services.roundcube = {
|
||||||
|
enable = true;
|
||||||
|
# this is the url of the vhost, not necessarily the same as the fqdn of
|
||||||
|
# the mailserver
|
||||||
|
hostName = "${cfg.subdomain}.${config.selfprivacy.domain}";
|
||||||
|
extraConfig = ''
|
||||||
|
# starttls needed for authentication, so the fqdn required to match
|
||||||
|
# the certificate
|
||||||
|
$config['smtp_host'] = "tls://${config.mailserver.fqdn}";
|
||||||
|
$config['smtp_user'] = "%u";
|
||||||
|
$config['smtp_pass'] = "%p";
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
services.nginx.virtualHosts."${cfg.subdomain}.${domain}" = {
|
||||||
|
forceSSL = true;
|
||||||
|
useACMEHost = domain;
|
||||||
|
enableACME = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.slices.roundcube.description = "Roundcube service slice";
|
||||||
|
# Roundcube depends on Dovecot and its OAuth2 client secret.
|
||||||
|
systemd.services.phpfpm-roundcube.after = [ "dovecot2.service" ];
|
||||||
|
}
|
||||||
|
# the following part is active only when "auth" module is enabled
|
||||||
|
(lib.mkIf is-auth-enabled {
|
||||||
|
services.roundcube.extraConfig = lib.mkAfter ''
|
||||||
|
$config['oauth_provider'] = 'generic';
|
||||||
|
$config['oauth_provider_name'] = '${auth-passthru.oauth2-provider-name}';
|
||||||
|
$config['oauth_client_id'] = '${oauth-donor.oauth-client-id}';
|
||||||
|
$config['oauth_client_secret'] = file_get_contents('${oauthClientSecretFP}');
|
||||||
|
$config['oauth_auth_uri'] = 'https://${auth-fqdn}/ui/oauth2';
|
||||||
|
$config['oauth_token_uri'] = 'https://${auth-fqdn}/oauth2/token';
|
||||||
|
$config['oauth_identity_uri'] = 'https://${auth-fqdn}/oauth2/openid/${oauth-donor.oauth-client-id}/userinfo';
|
||||||
|
$config['oauth_scope'] = 'email profile openid';
|
||||||
|
$config['oauth_auth_parameters'] = [];
|
||||||
|
$config['oauth_identity_fields'] = ['email'];
|
||||||
|
$config['oauth_login_redirect'] = true;
|
||||||
|
$config['auto_create_user'] = true;
|
||||||
|
'';
|
||||||
|
systemd.services.roundcube = {
|
||||||
|
after = [ "dovecot2.service" ];
|
||||||
|
requires = [ "dovecot2.service" ];
|
||||||
|
};
|
||||||
|
systemd.services.kanidm.serviceConfig.ExecStartPre = lib.mkAfter [
|
||||||
|
("-+" + kanidmExecStartPreScriptRoot)
|
||||||
|
];
|
||||||
|
|
||||||
|
selfprivacy.auth.clients."${oauth-donor.oauth-client-id}" = {
|
||||||
|
inherit adminsGroup usersGroup;
|
||||||
|
imageFile = ./icon.svg;
|
||||||
|
displayName = "Roundcube";
|
||||||
|
subdomain = cfg.subdomain;
|
||||||
|
isTokenNeeded = false;
|
||||||
|
isMailserver = true;
|
||||||
|
originUrl = "https://${cfg.subdomain}.${domain}/index.php/login/oauth";
|
||||||
|
originLanding = "https://${cfg.subdomain}.${domain}/";
|
||||||
|
useShortPreferredUsername = false;
|
||||||
|
clientSystemdUnits = [
|
||||||
|
"dovecot2.service"
|
||||||
|
"phpfpm-roundcube.service"
|
||||||
|
];
|
||||||
|
enablePkce = false;
|
||||||
|
linuxUserOfClient = linuxUserOfService;
|
||||||
|
linuxGroupOfClient = linuxGroupOfService;
|
||||||
|
scopeMaps = {
|
||||||
|
"${usersGroup}" = [
|
||||||
|
"email"
|
||||||
|
"openid"
|
||||||
|
"profile"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,14 @@
|
|||||||
{ mailserver-service-account-name
|
{
|
||||||
, mailserver-service-account-token-name
|
mailserver-service-account-name,
|
||||||
, mailserver-service-account-token-fp
|
mailserver-service-account-token-name,
|
||||||
|
mailserver-service-account-token-fp,
|
||||||
}:
|
}:
|
||||||
{ config, lib, pkgs, ... }@nixos-args:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}@nixos-args:
|
||||||
let
|
let
|
||||||
inherit (import ./common.nix nixos-args)
|
inherit (import ./common.nix nixos-args)
|
||||||
appendSetting
|
appendSetting
|
||||||
@@ -17,67 +23,70 @@ let
|
|||||||
keysPath = auth-passthru.keys-path;
|
keysPath = auth-passthru.keys-path;
|
||||||
|
|
||||||
# create service account token, needed for LDAP
|
# create service account token, needed for LDAP
|
||||||
kanidmExecStartPostScript = pkgs.writeShellScript
|
kanidmExecStartPostScript = pkgs.writeShellScript "mailserver-kanidm-ExecStartPost-script.sh" ''
|
||||||
"mailserver-kanidm-ExecStartPost-script.sh"
|
export HOME=$RUNTIME_DIRECTORY/client_home
|
||||||
''
|
readonly KANIDM="${pkgs.kanidm}/bin/kanidm"
|
||||||
export HOME=$RUNTIME_DIRECTORY/client_home
|
|
||||||
readonly KANIDM="${pkgs.kanidm}/bin/kanidm"
|
|
||||||
|
|
||||||
# get Kanidm service account for mailserver
|
# get Kanidm service account for mailserver
|
||||||
KANIDM_SERVICE_ACCOUNT="$($KANIDM service-account list --name idm_admin | grep -E "^name: ${mailserver-service-account-name}$")"
|
KANIDM_SERVICE_ACCOUNT="$($KANIDM service-account list --name idm_admin | grep -E "^name: ${mailserver-service-account-name}$")"
|
||||||
echo KANIDM_SERVICE_ACCOUNT: "$KANIDM_SERVICE_ACCOUNT"
|
echo KANIDM_SERVICE_ACCOUNT: "$KANIDM_SERVICE_ACCOUNT"
|
||||||
if [ -n "$KANIDM_SERVICE_ACCOUNT" ]
|
if [ -n "$KANIDM_SERVICE_ACCOUNT" ]
|
||||||
then
|
then
|
||||||
echo "kanidm service account \"${mailserver-service-account-name}\" is found"
|
echo "kanidm service account \"${mailserver-service-account-name}\" is found"
|
||||||
else
|
else
|
||||||
echo "kanidm service account \"${mailserver-service-account-name}\" is not found"
|
echo "kanidm service account \"${mailserver-service-account-name}\" is not found"
|
||||||
echo "creating new kanidm service account \"${mailserver-service-account-name}\""
|
echo "creating new kanidm service account \"${mailserver-service-account-name}\""
|
||||||
if $KANIDM service-account create --name idm_admin ${mailserver-service-account-name} ${mailserver-service-account-name} idm_admin
|
if $KANIDM service-account create --name idm_admin ${mailserver-service-account-name} ${mailserver-service-account-name} idm_admin
|
||||||
then
|
then
|
||||||
"kanidm service account \"${mailserver-service-account-name}\" created"
|
"kanidm service account \"${mailserver-service-account-name}\" created"
|
||||||
else
|
else
|
||||||
echo "error: cannot create kanidm service account \"${mailserver-service-account-name}\""
|
echo "error: cannot create kanidm service account \"${mailserver-service-account-name}\""
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# add Kanidm service account to `idm_mail_servers` group
|
# add Kanidm service account to `idm_mail_servers` group
|
||||||
$KANIDM group add-members idm_mail_servers ${mailserver-service-account-name}
|
$KANIDM group add-members idm_mail_servers ${mailserver-service-account-name}
|
||||||
|
|
||||||
# create a new read-only token for mailserver
|
# create a new read-only token for mailserver
|
||||||
if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin ${mailserver-service-account-name} ${mailserver-service-account-token-name} --output json)"
|
if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin ${mailserver-service-account-name} ${mailserver-service-account-token-name} --output json)"
|
||||||
then
|
then
|
||||||
echo "error: kanidm CLI returns an error when trying to generate service-account api-token"
|
echo "error: kanidm CLI returns an error when trying to generate service-account api-token"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if ! KANIDM_SERVICE_ACCOUNT_TOKEN="$(echo "$KANIDM_SERVICE_ACCOUNT_TOKEN_JSON" | ${lib.getExe pkgs.jq} -r .result)"
|
if ! KANIDM_SERVICE_ACCOUNT_TOKEN="$(echo "$KANIDM_SERVICE_ACCOUNT_TOKEN_JSON" | ${lib.getExe pkgs.jq} -r .result)"
|
||||||
then
|
then
|
||||||
echo "error: cannot get service-account API token from JSON"
|
echo "error: cannot get service-account API token from JSON"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! install --mode=640 \
|
if ! install --mode=640 \
|
||||||
<(printf "%s" "$KANIDM_SERVICE_ACCOUNT_TOKEN") \
|
<(printf "%s" "$KANIDM_SERVICE_ACCOUNT_TOKEN") \
|
||||||
${mailserver-service-account-token-fp}
|
${mailserver-service-account-token-fp}
|
||||||
then
|
then
|
||||||
echo "error: cannot write token to \"${mailserver-service-account-token-fp}\""
|
echo "error: cannot write token to \"${mailserver-service-account-token-fp}\""
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
|
|
||||||
ldapConfFile = "/run/${runtime-folder}/dovecot-ldap.conf.ext";
|
ldapConfFile = "/run/${runtime-folder}/dovecot-ldap.conf.ext";
|
||||||
mkLdapSearchScope = scope: (
|
mkLdapSearchScope =
|
||||||
if scope == "sub" then "subtree"
|
scope:
|
||||||
else if scope == "one" then "onelevel"
|
(
|
||||||
else scope
|
if scope == "sub" then
|
||||||
);
|
"subtree"
|
||||||
|
else if scope == "one" then
|
||||||
|
"onelevel"
|
||||||
|
else
|
||||||
|
scope
|
||||||
|
);
|
||||||
dovecot-ldap-config = pkgs.writeTextFile {
|
dovecot-ldap-config = pkgs.writeTextFile {
|
||||||
name = "dovecot-ldap.conf.ext.template";
|
name = "dovecot-ldap.conf.ext.template";
|
||||||
text = ''
|
text = ''
|
||||||
ldap_version = 3
|
ldap_version = 3
|
||||||
uris = ${lib.concatStringsSep " " config.mailserver.ldap.uris}
|
uris = ${lib.concatStringsSep " " config.mailserver.ldap.uris}
|
||||||
${lib.optionalString config.mailserver.ldap.startTls ''
|
${lib.optionalString config.mailserver.ldap.startTls ''
|
||||||
tls = yes
|
tls = yes
|
||||||
''}
|
''}
|
||||||
tls_require_cert = hard
|
tls_require_cert = hard
|
||||||
tls_ca_cert_file = ${config.mailserver.ldap.tlsCAFile}
|
tls_ca_cert_file = ${config.mailserver.ldap.tlsCAFile}
|
||||||
@@ -87,7 +96,7 @@ let
|
|||||||
base = ${config.mailserver.ldap.searchBase}
|
base = ${config.mailserver.ldap.searchBase}
|
||||||
scope = ${mkLdapSearchScope config.mailserver.ldap.searchScope}
|
scope = ${mkLdapSearchScope config.mailserver.ldap.searchScope}
|
||||||
${lib.optionalString (config.mailserver.ldap.dovecot.userAttrs != null) ''
|
${lib.optionalString (config.mailserver.ldap.dovecot.userAttrs != null) ''
|
||||||
user_attrs = ${config.mailserver.ldap.dovecot.userAttrs}
|
user_attrs = ${config.mailserver.ldap.dovecot.userAttrs}
|
||||||
''}
|
''}
|
||||||
user_filter = ${config.mailserver.ldap.dovecot.userFilter}
|
user_filter = ${config.mailserver.ldap.dovecot.userFilter}
|
||||||
'';
|
'';
|
||||||
@@ -101,10 +110,8 @@ let
|
|||||||
destination = ldapConfFile;
|
destination = ldapConfFile;
|
||||||
};
|
};
|
||||||
oauth-client-id = "mailserver";
|
oauth-client-id = "mailserver";
|
||||||
oauth-client-secret-fp =
|
oauth-client-secret-fp = "${keysPath}/${group}/kanidm-oauth-client-secret";
|
||||||
"${keysPath}/${group}/kanidm-oauth-client-secret";
|
oauth-secret-ExecStartPreScript = pkgs.writeShellScript "${oauth-client-id}-kanidm-ExecStartPre-script.sh" ''
|
||||||
oauth-secret-ExecStartPreScript = pkgs.writeShellScript
|
|
||||||
"${oauth-client-id}-kanidm-ExecStartPre-script.sh" ''
|
|
||||||
set -o xtrace
|
set -o xtrace
|
||||||
[ -f "${oauth-client-secret-fp}" ] || \
|
[ -f "${oauth-client-secret-fp}" ] || \
|
||||||
"${lib.getExe pkgs.openssl}" rand -base64 32 | tr "\n:@/+=" "012345" > "${oauth-client-secret-fp}"
|
"${lib.getExe pkgs.openssl}" rand -base64 32 | tr "\n:@/+=" "012345" > "${oauth-client-secret-fp}"
|
||||||
@@ -122,8 +129,8 @@ let
|
|||||||
openid_configuration_url = ${auth-passthru.oauth2-discovery-url oauth-client-id}
|
openid_configuration_url = ${auth-passthru.oauth2-discovery-url oauth-client-id}
|
||||||
debug = "no"
|
debug = "no"
|
||||||
'';
|
'';
|
||||||
prefix = ''introspection_url = "'' +
|
prefix =
|
||||||
(auth-passthru.oauth2-introspection-url-prefix oauth-client-id);
|
''introspection_url = "'' + (auth-passthru.oauth2-introspection-url-prefix oauth-client-id);
|
||||||
suffix = auth-passthru.oauth2-introspection-url-postfix + ''"'';
|
suffix = auth-passthru.oauth2-introspection-url-postfix + ''"'';
|
||||||
passwordFile = oauth-client-secret-fp;
|
passwordFile = oauth-client-secret-fp;
|
||||||
destination = dovecot-oauth2-conf-fp;
|
destination = dovecot-oauth2-conf-fp;
|
||||||
|
@@ -1,4 +1,9 @@
|
|||||||
{ config, lib, pkgs, ... }@nixos-args:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}@nixos-args:
|
||||||
let
|
let
|
||||||
inherit (import ./common.nix nixos-args)
|
inherit (import ./common.nix nixos-args)
|
||||||
appendSetting
|
appendSetting
|
||||||
@@ -9,8 +14,7 @@ let
|
|||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
|
|
||||||
ldapSenderLoginMapFile = "/run/postfix/ldap-sender-login-map.cf";
|
ldapSenderLoginMapFile = "/run/postfix/ldap-sender-login-map.cf";
|
||||||
submissionOptions.smtpd_sender_login_maps =
|
submissionOptions.smtpd_sender_login_maps = lib.mkForce "hash:/etc/postfix/vaccounts,ldap:${ldapSenderLoginMapFile}";
|
||||||
lib.mkForce "hash:/etc/postfix/vaccounts,ldap:${ldapSenderLoginMapFile}";
|
|
||||||
commonLdapConfig = ''
|
commonLdapConfig = ''
|
||||||
server_host = ${lib.concatStringsSep " " cfg.ldap.uris}
|
server_host = ${lib.concatStringsSep " " cfg.ldap.uris}
|
||||||
start_tls = ${if cfg.ldap.startTls then "yes" else "no"}
|
start_tls = ${if cfg.ldap.startTls then "yes" else "no"}
|
||||||
@@ -61,8 +65,10 @@ in
|
|||||||
${appendPwdInVirtualMailboxMap}
|
${appendPwdInVirtualMailboxMap}
|
||||||
${appendPwdInSenderLoginMap}
|
${appendPwdInSenderLoginMap}
|
||||||
'';
|
'';
|
||||||
restartTriggers =
|
restartTriggers = [
|
||||||
[ appendPwdInVirtualMailboxMap appendPwdInSenderLoginMap ];
|
appendPwdInVirtualMailboxMap
|
||||||
|
appendPwdInSenderLoginMap
|
||||||
|
];
|
||||||
wants = [ auth-passthru.oauth2-systemd-service ];
|
wants = [ auth-passthru.oauth2-systemd-service ];
|
||||||
after = [ auth-passthru.oauth2-systemd-service ];
|
after = [ auth-passthru.oauth2-systemd-service ];
|
||||||
};
|
};
|
||||||
|
@@ -4,11 +4,17 @@ rec {
|
|||||||
domain = config.selfprivacy.domain;
|
domain = config.selfprivacy.domain;
|
||||||
group = "dovecot2";
|
group = "dovecot2";
|
||||||
is-auth-enabled =
|
is-auth-enabled =
|
||||||
config.selfprivacy.modules.simple-nixos-mailserver.enableSso
|
config.selfprivacy.modules.simple-nixos-mailserver.enableSso && config.selfprivacy.sso.enable;
|
||||||
&& config.selfprivacy.sso.enable;
|
|
||||||
|
|
||||||
appendSetting =
|
appendSetting =
|
||||||
{ name, file, prefix, suffix ? "", passwordFile, destination }:
|
{
|
||||||
|
name,
|
||||||
|
file,
|
||||||
|
prefix,
|
||||||
|
suffix ? "",
|
||||||
|
passwordFile,
|
||||||
|
destination,
|
||||||
|
}:
|
||||||
pkgs.writeScript "append-ldap-bind-pwd-in-${name}" ''
|
pkgs.writeScript "append-ldap-bind-pwd-in-${name}" ''
|
||||||
#!${pkgs.stdenv.shell}
|
#!${pkgs.stdenv.shell}
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
@@ -1,4 +1,9 @@
|
|||||||
{ config, lib, pkgs, ... }@nixos-args:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}@nixos-args:
|
||||||
let
|
let
|
||||||
sp = config.selfprivacy;
|
sp = config.selfprivacy;
|
||||||
|
|
||||||
@@ -11,26 +16,22 @@ let
|
|||||||
mailserver-service-account = {
|
mailserver-service-account = {
|
||||||
mailserver-service-account-name = "sp.mailserver.service-account";
|
mailserver-service-account-name = "sp.mailserver.service-account";
|
||||||
mailserver-service-account-token-name = "mailserver-service-account-token";
|
mailserver-service-account-token-name = "mailserver-service-account-token";
|
||||||
mailserver-service-account-token-fp =
|
mailserver-service-account-token-fp = "/run/keys/${group}/kanidm-service-account-token"; # FIXME sync with auth module
|
||||||
"/run/keys/${group}/kanidm-service-account-token"; # FIXME sync with auth module
|
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
lib.mkIf sp.modules.simple-nixos-mailserver.enable (lib.mkMerge [
|
lib.mkIf sp.modules.simple-nixos-mailserver.enable (
|
||||||
{
|
lib.mkMerge [
|
||||||
assertions = [
|
{
|
||||||
{
|
assertions = [
|
||||||
assertion =
|
{
|
||||||
config.selfprivacy.modules.simple-nixos-mailserver.enableSso
|
assertion =
|
||||||
-> config.selfprivacy.sso.enable;
|
config.selfprivacy.modules.simple-nixos-mailserver.enableSso -> config.selfprivacy.sso.enable;
|
||||||
message =
|
message = "SSO cannot be enabled for Mailserver when SSO is disabled globally.";
|
||||||
"SSO cannot be enabled for Mailserver when SSO is disabled globally.";
|
}
|
||||||
}
|
];
|
||||||
];
|
fileSystems = lib.mkIf sp.useBinds {
|
||||||
fileSystems = lib.mkIf sp.useBinds
|
|
||||||
{
|
|
||||||
"/var/vmail" = {
|
"/var/vmail" = {
|
||||||
device =
|
device = "/volumes/${sp.modules.simple-nixos-mailserver.location}/vmail";
|
||||||
"/volumes/${sp.modules.simple-nixos-mailserver.location}/vmail";
|
|
||||||
options = [
|
options = [
|
||||||
"bind"
|
"bind"
|
||||||
"x-systemd.required-by=postfix.service"
|
"x-systemd.required-by=postfix.service"
|
||||||
@@ -38,8 +39,7 @@ lib.mkIf sp.modules.simple-nixos-mailserver.enable (lib.mkMerge [
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
"/var/sieve" = {
|
"/var/sieve" = {
|
||||||
device =
|
device = "/volumes/${sp.modules.simple-nixos-mailserver.location}/sieve";
|
||||||
"/volumes/${sp.modules.simple-nixos-mailserver.location}/sieve";
|
|
||||||
options = [
|
options = [
|
||||||
"bind"
|
"bind"
|
||||||
"x-systemd.required-by=dovecot2.service"
|
"x-systemd.required-by=dovecot2.service"
|
||||||
@@ -48,114 +48,120 @@ lib.mkIf sp.modules.simple-nixos-mailserver.enable (lib.mkMerge [
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
users.users = {
|
users.users = {
|
||||||
virtualMail = {
|
virtualMail = {
|
||||||
isNormalUser = false;
|
isNormalUser = false;
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
users.groups.acmereceivers.members = [ "dovecot2" "postfix" "virtualMail" ];
|
|
||||||
|
|
||||||
mailserver = {
|
|
||||||
enable = true;
|
|
||||||
fqdn = sp.domain;
|
|
||||||
domains = [ sp.domain ];
|
|
||||||
localDnsResolver = false;
|
|
||||||
|
|
||||||
# A list of all login accounts. To create the password hashes, use
|
|
||||||
# mkpasswd -m sha-512 "super secret password"
|
|
||||||
loginAccounts = ({
|
|
||||||
"${sp.username}@${sp.domain}" = {
|
|
||||||
hashedPassword = sp.hashedMasterPassword;
|
|
||||||
sieveScript = ''
|
|
||||||
require ["fileinto", "mailbox"];
|
|
||||||
if header :contains "Chat-Version" "1.0"
|
|
||||||
{
|
|
||||||
fileinto :create "DeltaChat";
|
|
||||||
stop;
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
} // builtins.listToAttrs (builtins.map
|
|
||||||
(user: {
|
|
||||||
name = "${user.username}@${sp.domain}";
|
|
||||||
value = {
|
|
||||||
hashedPassword = user.hashedPassword;
|
|
||||||
sieveScript = ''
|
|
||||||
require ["fileinto", "mailbox"];
|
|
||||||
if header :contains "Chat-Version" "1.0"
|
|
||||||
{
|
|
||||||
fileinto :create "DeltaChat";
|
|
||||||
stop;
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
})
|
|
||||||
sp.users));
|
|
||||||
|
|
||||||
extraVirtualAliases = {
|
|
||||||
"admin@${sp.domain}" = "${sp.username}@${sp.domain}";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
certificateScheme = "manual";
|
users.groups.acmereceivers.members = [
|
||||||
certificateFile = "/var/lib/acme/root-${sp.domain}/fullchain.pem";
|
"dovecot2"
|
||||||
keyFile = "/var/lib/acme/root-${sp.domain}/key.pem";
|
"postfix"
|
||||||
|
"virtualMail"
|
||||||
|
];
|
||||||
|
|
||||||
# Enable IMAP and POP3
|
mailserver = {
|
||||||
enableImap = true;
|
enable = true;
|
||||||
enableImapSsl = true;
|
fqdn = sp.domain;
|
||||||
enablePop3 = false;
|
domains = [ sp.domain ];
|
||||||
enablePop3Ssl = false;
|
localDnsResolver = false;
|
||||||
dkimSelector = "selector";
|
|
||||||
|
|
||||||
# Enable the ManageSieve protocol
|
# A list of all login accounts. To create the password hashes, use
|
||||||
enableManageSieve = true;
|
# mkpasswd -m sha-512 "super secret password"
|
||||||
|
loginAccounts = (
|
||||||
|
{
|
||||||
|
"${sp.username}@${sp.domain}" = {
|
||||||
|
hashedPassword = sp.hashedMasterPassword;
|
||||||
|
sieveScript = ''
|
||||||
|
require ["fileinto", "mailbox"];
|
||||||
|
if header :contains "Chat-Version" "1.0"
|
||||||
|
{
|
||||||
|
fileinto :create "DeltaChat";
|
||||||
|
stop;
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// builtins.listToAttrs (
|
||||||
|
builtins.map (user: {
|
||||||
|
name = "${user.username}@${sp.domain}";
|
||||||
|
value = {
|
||||||
|
hashedPassword = user.hashedPassword;
|
||||||
|
sieveScript = ''
|
||||||
|
require ["fileinto", "mailbox"];
|
||||||
|
if header :contains "Chat-Version" "1.0"
|
||||||
|
{
|
||||||
|
fileinto :create "DeltaChat";
|
||||||
|
stop;
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}) sp.users
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
virusScanning = false;
|
extraVirtualAliases = {
|
||||||
|
"admin@${sp.domain}" = "${sp.username}@${sp.domain}";
|
||||||
|
};
|
||||||
|
|
||||||
mailDirectory = "/var/vmail";
|
certificateScheme = "manual";
|
||||||
};
|
certificateFile = "/var/lib/acme/root-${sp.domain}/fullchain.pem";
|
||||||
|
keyFile = "/var/lib/acme/root-${sp.domain}/key.pem";
|
||||||
|
|
||||||
systemd = {
|
# Enable IMAP and POP3
|
||||||
services = {
|
enableImap = true;
|
||||||
dovecot2.serviceConfig.Slice = "simple_nixos_mailserver.slice";
|
enableImapSsl = true;
|
||||||
postfix.serviceConfig.Slice = "simple_nixos_mailserver.slice";
|
enablePop3 = false;
|
||||||
rspamd.serviceConfig.Slice = "simple_nixos_mailserver.slice";
|
enablePop3Ssl = false;
|
||||||
redis-rspamd.serviceConfig.Slice = "simple_nixos_mailserver.slice";
|
dkimSelector = "selector";
|
||||||
opendkim.serviceConfig.Slice = "simple_nixos_mailserver.slice";
|
|
||||||
|
# Enable the ManageSieve protocol
|
||||||
|
enableManageSieve = true;
|
||||||
|
|
||||||
|
virusScanning = false;
|
||||||
|
|
||||||
|
mailDirectory = "/var/vmail";
|
||||||
};
|
};
|
||||||
slices."simple_nixos_mailserver" = {
|
|
||||||
name = "simple_nixos_mailserver.slice";
|
systemd = {
|
||||||
description = "Simple NixOS Mailserver service slice";
|
services = {
|
||||||
|
dovecot2.serviceConfig.Slice = "simple_nixos_mailserver.slice";
|
||||||
|
postfix.serviceConfig.Slice = "simple_nixos_mailserver.slice";
|
||||||
|
rspamd.serviceConfig.Slice = "simple_nixos_mailserver.slice";
|
||||||
|
redis-rspamd.serviceConfig.Slice = "simple_nixos_mailserver.slice";
|
||||||
|
opendkim.serviceConfig.Slice = "simple_nixos_mailserver.slice";
|
||||||
|
};
|
||||||
|
slices."simple_nixos_mailserver" = {
|
||||||
|
name = "simple_nixos_mailserver.slice";
|
||||||
|
description = "Simple NixOS Mailserver service slice";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
}
|
# the following parts are active only when "auth" module is enabled
|
||||||
# the following parts are active only when "auth" module is enabled
|
(lib.mkIf is-auth-enabled {
|
||||||
(lib.mkIf is-auth-enabled {
|
mailserver = {
|
||||||
mailserver = {
|
extraVirtualAliases = lib.mkForce { };
|
||||||
extraVirtualAliases = lib.mkForce { };
|
loginAccounts = lib.mkForce { };
|
||||||
loginAccounts = lib.mkForce { };
|
# LDAP is needed for Postfix to query Kanidm about email address ownership.
|
||||||
# LDAP is needed for Postfix to query Kanidm about email address ownership.
|
# LDAP is needed for Dovecot also.
|
||||||
# LDAP is needed for Dovecot also.
|
ldap = {
|
||||||
ldap = {
|
# false; otherwise, simple-nixos-mailserver enables auth via LDAP
|
||||||
# false; otherwise, simple-nixos-mailserver enables auth via LDAP
|
enable = false;
|
||||||
enable = false;
|
|
||||||
|
|
||||||
# bind.dn = "uid=mail,ou=persons," + ldap_base_dn;
|
# bind.dn = "uid=mail,ou=persons," + ldap_base_dn;
|
||||||
bind.dn = "dn=token";
|
bind.dn = "dn=token";
|
||||||
# TODO change in this file should trigger system restart dovecot
|
# TODO change in this file should trigger system restart dovecot
|
||||||
bind.passwordFile =
|
bind.passwordFile = mailserver-service-account.mailserver-service-account-token-fp;
|
||||||
mailserver-service-account.mailserver-service-account-token-fp;
|
|
||||||
|
|
||||||
# searchBase = "ou=persons," + ldap_base_dn;
|
# searchBase = "ou=persons," + ldap_base_dn;
|
||||||
searchBase = auth-passthru.ldap-base-dn; # TODO refine this
|
searchBase = auth-passthru.ldap-base-dn; # TODO refine this
|
||||||
|
|
||||||
# NOTE: 127.0.0.1 instead of localhost doesn't work (maybe because of TLS)
|
# NOTE: 127.0.0.1 instead of localhost doesn't work (maybe because of TLS)
|
||||||
uris = [ "ldaps://localhost:${toString auth-passthru.ldap-port}" ];
|
uris = [ "ldaps://localhost:${toString auth-passthru.ldap-port}" ];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
})
|
||||||
})
|
(lib.mkIf is-auth-enabled (import ./auth-dovecot.nix mailserver-service-account nixos-args))
|
||||||
(lib.mkIf is-auth-enabled
|
(lib.mkIf is-auth-enabled (import ./auth-postfix.nix nixos-args))
|
||||||
(import ./auth-dovecot.nix mailserver-service-account nixos-args))
|
]
|
||||||
(lib.mkIf is-auth-enabled (import ./auth-postfix.nix nixos-args))
|
)
|
||||||
])
|
|
||||||
|
@@ -1,43 +1,45 @@
|
|||||||
{
|
{
|
||||||
description = "PoC SP module for the simple-nixos-mailserver";
|
description = "PoC SP module for the simple-nixos-mailserver";
|
||||||
|
|
||||||
inputs.mailserver.url =
|
inputs.mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver";
|
||||||
gitlab:simple-nixos-mailserver/nixos-mailserver;
|
|
||||||
|
|
||||||
outputs = { self, mailserver }: {
|
outputs =
|
||||||
nixosModules.default = _: {
|
{ self, mailserver }:
|
||||||
imports = [
|
{
|
||||||
mailserver.nixosModules.default
|
nixosModules.default = _: {
|
||||||
./options.nix
|
imports = [
|
||||||
./config.nix
|
mailserver.nixosModules.default
|
||||||
];
|
./options.nix
|
||||||
};
|
./config.nix
|
||||||
configPathsNeeded =
|
];
|
||||||
builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
|
};
|
||||||
meta = { lib, ... }: {
|
configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
|
||||||
spModuleSchemaVersion = 1;
|
meta =
|
||||||
id = "simple-nixos-mailserver";
|
{ lib, ... }:
|
||||||
name = "Mail Server";
|
{
|
||||||
description = "E-Mail for company and family.";
|
spModuleSchemaVersion = 1;
|
||||||
svgIcon = builtins.readFile ./icon.svg;
|
id = "simple-nixos-mailserver";
|
||||||
isMovable = true;
|
name = "Mail Server";
|
||||||
isRequired = true;
|
description = "E-Mail for company and family.";
|
||||||
canBeBackedUp = true;
|
svgIcon = builtins.readFile ./icon.svg;
|
||||||
backupDescription = "Mail boxes and filters.";
|
isMovable = true;
|
||||||
systemdServices = [
|
isRequired = true;
|
||||||
"dovecot2.service"
|
canBeBackedUp = true;
|
||||||
"postfix.service"
|
backupDescription = "Mail boxes and filters.";
|
||||||
];
|
systemdServices = [
|
||||||
user = "virtualMail";
|
"dovecot2.service"
|
||||||
folders = [
|
"postfix.service"
|
||||||
"/var/vmail"
|
];
|
||||||
"/var/sieve"
|
user = "virtualMail";
|
||||||
];
|
folders = [
|
||||||
supportLevel = "normal";
|
"/var/vmail"
|
||||||
};
|
"/var/sieve"
|
||||||
|
];
|
||||||
|
supportLevel = "normal";
|
||||||
|
};
|
||||||
|
|
||||||
# TODO generate json docs from module? something like:
|
# TODO generate json docs from module? something like:
|
||||||
# nix eval --impure --expr 'let flake = builtins.getFlake (builtins.toPath ./.); pkgs = flake.inputs.mailserver.inputs.nixpkgs.legacyPackages.x86_64-linux; in (pkgs.nixosOptionsDoc { inherit (pkgs.lib.evalModules { modules = [ flake.nixosModules.default ]; }) options; }).optionsJSON'
|
# nix eval --impure --expr 'let flake = builtins.getFlake (builtins.toPath ./.); pkgs = flake.inputs.mailserver.inputs.nixpkgs.legacyPackages.x86_64-linux; in (pkgs.nixosOptionsDoc { inherit (pkgs.lib.evalModules { modules = [ flake.nixosModules.default ]; }) options; }).optionsJSON'
|
||||||
# (doesn't work because of `assertions`)
|
# (doesn't work because of `assertions`)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -1,31 +1,37 @@
|
|||||||
{ lib, ... }:
|
{ lib, ... }:
|
||||||
{
|
{
|
||||||
options.selfprivacy.modules.simple-nixos-mailserver = {
|
options.selfprivacy.modules.simple-nixos-mailserver = {
|
||||||
enable = (lib.mkOption {
|
enable =
|
||||||
default = false;
|
(lib.mkOption {
|
||||||
type = lib.types.bool;
|
default = false;
|
||||||
description = "Enable mail server";
|
type = lib.types.bool;
|
||||||
}) // {
|
description = "Enable mail server";
|
||||||
meta = {
|
})
|
||||||
type = "enable";
|
// {
|
||||||
|
meta = {
|
||||||
|
type = "enable";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
location =
|
||||||
location = (lib.mkOption {
|
(lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Location";
|
description = "Location";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "location";
|
meta = {
|
||||||
|
type = "location";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
enableSso =
|
||||||
enableSso = (lib.mkOption {
|
(lib.mkOption {
|
||||||
default = true;
|
default = true;
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
description = "Enable SSO for mail server";
|
description = "Enable SSO for mail server";
|
||||||
}) // {
|
})
|
||||||
meta = {
|
// {
|
||||||
type = "enable";
|
meta = {
|
||||||
|
type = "enable";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -5,37 +5,40 @@
|
|||||||
nixpkgs-24-11.url = "github:NixOS/nixpkgs/nixos-24.11";
|
nixpkgs-24-11.url = "github:NixOS/nixpkgs/nixos-24.11";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = {nixpkgs-24-11, ...}: {
|
outputs =
|
||||||
nixosModules.default = import ./module.nix nixpkgs-24-11.legacyPackages.x86_64-linux;
|
{ nixpkgs-24-11, ... }:
|
||||||
configPathsNeeded =
|
{
|
||||||
builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
|
nixosModules.default = import ./module.nix nixpkgs-24-11.legacyPackages.x86_64-linux;
|
||||||
meta = {lib, ...}: {
|
configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
|
||||||
spModuleSchemaVersion = 1;
|
meta =
|
||||||
id = "vikunja";
|
{ lib, ... }:
|
||||||
name = "Vikunja";
|
{
|
||||||
description = "Vikunja, the fluffy, open-source, self-hostable to-do app.";
|
spModuleSchemaVersion = 1;
|
||||||
svgIcon = builtins.readFile ./icon.svg;
|
id = "vikunja";
|
||||||
isMovable = true;
|
name = "Vikunja";
|
||||||
isRequired = false;
|
description = "Vikunja, the fluffy, open-source, self-hostable to-do app.";
|
||||||
backupDescription = "Tasks and attachments.";
|
svgIcon = builtins.readFile ./icon.svg;
|
||||||
systemdServices = [
|
isMovable = true;
|
||||||
"vikunja.service"
|
isRequired = false;
|
||||||
];
|
backupDescription = "Tasks and attachments.";
|
||||||
folders = [
|
systemdServices = [
|
||||||
"/var/lib/vikunja"
|
"vikunja.service"
|
||||||
];
|
];
|
||||||
postgreDatabases = [
|
folders = [
|
||||||
"vikunja"
|
"/var/lib/vikunja"
|
||||||
];
|
];
|
||||||
license = [
|
postgreDatabases = [
|
||||||
lib.licenses.agpl3Plus
|
"vikunja"
|
||||||
];
|
];
|
||||||
homepage = "https://vikunja.io";
|
license = [
|
||||||
sourcePage = "https://github.com/go-vikunja/vikunja";
|
lib.licenses.agpl3Plus
|
||||||
supportLevel = "normal";
|
];
|
||||||
sso = {
|
homepage = "https://vikunja.io";
|
||||||
userGroup = "sp.vikunja.users";
|
sourcePage = "https://github.com/go-vikunja/vikunja";
|
||||||
};
|
supportLevel = "normal";
|
||||||
|
sso = {
|
||||||
|
userGroup = "sp.vikunja.users";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
latestPkgs: {
|
latestPkgs:
|
||||||
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
...
|
...
|
||||||
}: let
|
}:
|
||||||
|
let
|
||||||
sp = config.selfprivacy;
|
sp = config.selfprivacy;
|
||||||
cfg = sp.modules.vikunja;
|
cfg = sp.modules.vikunja;
|
||||||
oauthClientID = "vikunja";
|
oauthClientID = "vikunja";
|
||||||
@@ -13,18 +15,16 @@ latestPkgs: {
|
|||||||
# SelfPrivacy uses SP Module ID to identify the group!
|
# SelfPrivacy uses SP Module ID to identify the group!
|
||||||
usersGroup = "sp.vikunja.users";
|
usersGroup = "sp.vikunja.users";
|
||||||
|
|
||||||
oauthClientSecretFP =
|
oauthClientSecretFP = auth-passthru.mkOAuth2ClientSecretFP oauthClientID;
|
||||||
auth-passthru.mkOAuth2ClientSecretFP oauthClientID;
|
|
||||||
|
|
||||||
vikunjaPackage = latestPkgs.vikunja.overrideAttrs (old: {
|
vikunjaPackage = latestPkgs.vikunja.overrideAttrs (old: {
|
||||||
doCheck = false; # Tests are slow.
|
doCheck = false; # Tests are slow.
|
||||||
patches =
|
patches = (old.patches or [ ]) ++ [
|
||||||
(old.patches or [])
|
./load-client-secret-from-env.patch
|
||||||
++ [
|
];
|
||||||
./load-client-secret-from-env.patch
|
|
||||||
];
|
|
||||||
});
|
});
|
||||||
in {
|
in
|
||||||
|
{
|
||||||
options.selfprivacy.modules.vikunja = {
|
options.selfprivacy.modules.vikunja = {
|
||||||
enable =
|
enable =
|
||||||
(lib.mkOption {
|
(lib.mkOption {
|
||||||
@@ -63,157 +63,168 @@ in {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config =
|
config = lib.mkIf cfg.enable {
|
||||||
lib.mkIf cfg.enable
|
assertions = [
|
||||||
{
|
{
|
||||||
assertions = [
|
assertion = sp.sso.enable;
|
||||||
{
|
message = "Vikunja cannot be enabled when SSO is disabled.";
|
||||||
assertion = sp.sso.enable;
|
}
|
||||||
message = "Vikunja cannot be enabled when SSO is disabled.";
|
];
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
fileSystems = lib.mkIf sp.useBinds {
|
fileSystems = lib.mkIf sp.useBinds {
|
||||||
"/var/lib/vikunja" = {
|
"/var/lib/vikunja" = {
|
||||||
device = "/volumes/${cfg.location}/vikunja";
|
device = "/volumes/${cfg.location}/vikunja";
|
||||||
options = ["bind"];
|
options = [ "bind" ];
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
users = {
|
|
||||||
users.vikunja = {
|
|
||||||
isSystemUser = true;
|
|
||||||
group = "vikunja";
|
|
||||||
};
|
|
||||||
groups.vikunja = {};
|
|
||||||
};
|
|
||||||
|
|
||||||
services.postgresql = {
|
|
||||||
ensureDatabases = ["vikunja"];
|
|
||||||
ensureUsers = [
|
|
||||||
{
|
|
||||||
name = "vikunja";
|
|
||||||
ensureDBOwnership = true;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
services.vikunja = {
|
|
||||||
enable = true;
|
|
||||||
package = vikunjaPackage;
|
|
||||||
frontendScheme = "https";
|
|
||||||
frontendHostname = "${cfg.subdomain}.${sp.domain}";
|
|
||||||
port = 4835;
|
|
||||||
|
|
||||||
database = {
|
|
||||||
type = "postgres";
|
|
||||||
host = "/run/postgresql";
|
|
||||||
};
|
|
||||||
|
|
||||||
settings = {
|
|
||||||
service = {
|
|
||||||
enableregistration = false;
|
|
||||||
enabletotp = false;
|
|
||||||
enableuserdeletion = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
auth = {
|
|
||||||
local.enabled = false;
|
|
||||||
openid = {
|
|
||||||
enabled = true;
|
|
||||||
providers = [
|
|
||||||
{
|
|
||||||
name = oauth2-provider-name;
|
|
||||||
authurl = lib.strings.removeSuffix "/.well-known/openid-configuration" oauthDiscoveryURL;
|
|
||||||
clientid = oauthClientID;
|
|
||||||
clientsecret = ""; # There's patch for our Vikunja to make it load client secret from environment variable.
|
|
||||||
scope = "openid profile email";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = {
|
|
||||||
useACMEHost = sp.domain;
|
|
||||||
forceSSL = true;
|
|
||||||
extraConfig = ''
|
|
||||||
add_header Strict-Transport-Security $hsts_header;
|
|
||||||
#add_header Content-Security-Policy "script-src 'self'; object-src 'none'; base-uri 'none';" always;
|
|
||||||
add_header 'Referrer-Policy' 'origin-when-cross-origin';
|
|
||||||
add_header X-Frame-Options DENY;
|
|
||||||
add_header X-Content-Type-Options nosniff;
|
|
||||||
add_header X-XSS-Protection "1; mode=block";
|
|
||||||
proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict";
|
|
||||||
'';
|
|
||||||
locations = {
|
|
||||||
"/" = {
|
|
||||||
proxyPass = "http://127.0.0.1:4835";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd = {
|
|
||||||
services.vikunja = {
|
|
||||||
unitConfig.RequiresMountsFor = lib.mkIf sp.useBinds "/volumes/${cfg.location}/vikunja";
|
|
||||||
serviceConfig = {
|
|
||||||
Slice = "vikunja.slice";
|
|
||||||
LoadCredential = "oauth2-secret:${oauthClientSecretFP}";
|
|
||||||
DynamicUser = lib.mkForce false;
|
|
||||||
User = "vikunja";
|
|
||||||
Group = "vikunja";
|
|
||||||
|
|
||||||
AmbientCapabilities = [""];
|
|
||||||
|
|
||||||
LockPersonality = true;
|
|
||||||
MemoryDenyWriteExecute = true;
|
|
||||||
NoNewPrivileges = true;
|
|
||||||
PrivateDevices = true;
|
|
||||||
PrivateTmp = true;
|
|
||||||
|
|
||||||
PrivateUsers = true;
|
|
||||||
ProcSubset = "pid";
|
|
||||||
ProtectClock = true;
|
|
||||||
ProtectControlGroups = true;
|
|
||||||
ProtectHome = true;
|
|
||||||
ProtectHostname = true;
|
|
||||||
ProtectKernelLogs = true;
|
|
||||||
ProtectKernelModules = true;
|
|
||||||
ProtectKernelTunables = true;
|
|
||||||
|
|
||||||
ProtectProc = "invisible";
|
|
||||||
|
|
||||||
ProtectSystem = "strict";
|
|
||||||
|
|
||||||
RestrictAddressFamilies = ["AF_UNIX" "AF_INET" "AF_INET6"];
|
|
||||||
|
|
||||||
RestrictNamespaces = true;
|
|
||||||
RestrictRealtime = true;
|
|
||||||
RestrictSUIDSGID = true;
|
|
||||||
SystemCallArchitectures = "native";
|
|
||||||
|
|
||||||
RemoveIPC = true;
|
|
||||||
|
|
||||||
SystemCallFilter = ["@system-service" "~@cpu-emulation" "~@debug" "~@keyring" "~@memlock" "~@obsolete" "~@privileged" "~@setuid"];
|
|
||||||
};
|
|
||||||
environment.SP_VIKUNJA_CLIENT_SECRET_PATH = "%d/oauth2-secret";
|
|
||||||
};
|
|
||||||
slices.vikunja = {
|
|
||||||
description = "Vikunja service slice";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
selfprivacy.auth.clients.${oauthClientID} = {
|
|
||||||
inherit usersGroup;
|
|
||||||
subdomain = cfg.subdomain;
|
|
||||||
isTokenNeeded = false;
|
|
||||||
originLanding = "https://${cfg.subdomain}.${sp.domain}/";
|
|
||||||
originUrl = "https://${cfg.subdomain}.${sp.domain}/auth/openid/${lib.strings.toLower oauth2-provider-name}";
|
|
||||||
clientSystemdUnits = ["vikunja.service"];
|
|
||||||
enablePkce = false;
|
|
||||||
linuxUserOfClient = "vikunja";
|
|
||||||
linuxGroupOfClient = "vikunja";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
users = {
|
||||||
|
users.vikunja = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = "vikunja";
|
||||||
|
};
|
||||||
|
groups.vikunja = { };
|
||||||
|
};
|
||||||
|
|
||||||
|
services.postgresql = {
|
||||||
|
ensureDatabases = [ "vikunja" ];
|
||||||
|
ensureUsers = [
|
||||||
|
{
|
||||||
|
name = "vikunja";
|
||||||
|
ensureDBOwnership = true;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
services.vikunja = {
|
||||||
|
enable = true;
|
||||||
|
package = vikunjaPackage;
|
||||||
|
frontendScheme = "https";
|
||||||
|
frontendHostname = "${cfg.subdomain}.${sp.domain}";
|
||||||
|
port = 4835;
|
||||||
|
|
||||||
|
database = {
|
||||||
|
type = "postgres";
|
||||||
|
host = "/run/postgresql";
|
||||||
|
};
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
service = {
|
||||||
|
enableregistration = false;
|
||||||
|
enabletotp = false;
|
||||||
|
enableuserdeletion = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
auth = {
|
||||||
|
local.enabled = false;
|
||||||
|
openid = {
|
||||||
|
enabled = true;
|
||||||
|
providers = [
|
||||||
|
{
|
||||||
|
name = oauth2-provider-name;
|
||||||
|
authurl = lib.strings.removeSuffix "/.well-known/openid-configuration" oauthDiscoveryURL;
|
||||||
|
clientid = oauthClientID;
|
||||||
|
clientsecret = ""; # There's patch for our Vikunja to make it load client secret from environment variable.
|
||||||
|
scope = "openid profile email";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = {
|
||||||
|
useACMEHost = sp.domain;
|
||||||
|
forceSSL = true;
|
||||||
|
extraConfig = ''
|
||||||
|
add_header Strict-Transport-Security $hsts_header;
|
||||||
|
#add_header Content-Security-Policy "script-src 'self'; object-src 'none'; base-uri 'none';" always;
|
||||||
|
add_header 'Referrer-Policy' 'origin-when-cross-origin';
|
||||||
|
add_header X-Frame-Options DENY;
|
||||||
|
add_header X-Content-Type-Options nosniff;
|
||||||
|
add_header X-XSS-Protection "1; mode=block";
|
||||||
|
proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict";
|
||||||
|
'';
|
||||||
|
locations = {
|
||||||
|
"/" = {
|
||||||
|
proxyPass = "http://127.0.0.1:4835";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd = {
|
||||||
|
services.vikunja = {
|
||||||
|
unitConfig.RequiresMountsFor = lib.mkIf sp.useBinds "/volumes/${cfg.location}/vikunja";
|
||||||
|
serviceConfig = {
|
||||||
|
Slice = "vikunja.slice";
|
||||||
|
LoadCredential = "oauth2-secret:${oauthClientSecretFP}";
|
||||||
|
DynamicUser = lib.mkForce false;
|
||||||
|
User = "vikunja";
|
||||||
|
Group = "vikunja";
|
||||||
|
|
||||||
|
AmbientCapabilities = [ "" ];
|
||||||
|
|
||||||
|
LockPersonality = true;
|
||||||
|
MemoryDenyWriteExecute = true;
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
PrivateDevices = true;
|
||||||
|
PrivateTmp = true;
|
||||||
|
|
||||||
|
PrivateUsers = true;
|
||||||
|
ProcSubset = "pid";
|
||||||
|
ProtectClock = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
ProtectHome = true;
|
||||||
|
ProtectHostname = true;
|
||||||
|
ProtectKernelLogs = true;
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
ProtectKernelTunables = true;
|
||||||
|
|
||||||
|
ProtectProc = "invisible";
|
||||||
|
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
|
||||||
|
RestrictAddressFamilies = [
|
||||||
|
"AF_UNIX"
|
||||||
|
"AF_INET"
|
||||||
|
"AF_INET6"
|
||||||
|
];
|
||||||
|
|
||||||
|
RestrictNamespaces = true;
|
||||||
|
RestrictRealtime = true;
|
||||||
|
RestrictSUIDSGID = true;
|
||||||
|
SystemCallArchitectures = "native";
|
||||||
|
|
||||||
|
RemoveIPC = true;
|
||||||
|
|
||||||
|
SystemCallFilter = [
|
||||||
|
"@system-service"
|
||||||
|
"~@cpu-emulation"
|
||||||
|
"~@debug"
|
||||||
|
"~@keyring"
|
||||||
|
"~@memlock"
|
||||||
|
"~@obsolete"
|
||||||
|
"~@privileged"
|
||||||
|
"~@setuid"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
environment.SP_VIKUNJA_CLIENT_SECRET_PATH = "%d/oauth2-secret";
|
||||||
|
};
|
||||||
|
slices.vikunja = {
|
||||||
|
description = "Vikunja service slice";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
selfprivacy.auth.clients.${oauthClientID} = {
|
||||||
|
inherit usersGroup;
|
||||||
|
subdomain = cfg.subdomain;
|
||||||
|
isTokenNeeded = false;
|
||||||
|
originLanding = "https://${cfg.subdomain}.${sp.domain}/";
|
||||||
|
originUrl = "https://${cfg.subdomain}.${sp.domain}/auth/openid/${lib.strings.toLower oauth2-provider-name}";
|
||||||
|
clientSystemdUnits = [ "vikunja.service" ];
|
||||||
|
enablePkce = false;
|
||||||
|
linuxUserOfClient = "vikunja";
|
||||||
|
linuxGroupOfClient = "vikunja";
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
30
users.nix
30
users.nix
@@ -6,21 +6,23 @@ in
|
|||||||
users = {
|
users = {
|
||||||
mutableUsers = false;
|
mutableUsers = false;
|
||||||
allowNoPasswordLogin = true;
|
allowNoPasswordLogin = true;
|
||||||
users = {
|
users =
|
||||||
"${cfg.username}" = {
|
{
|
||||||
isNormalUser = true;
|
"${cfg.username}" = {
|
||||||
hashedPassword = cfg.hashedMasterPassword;
|
|
||||||
openssh.authorizedKeys.keys = cfg.sshKeys;
|
|
||||||
};
|
|
||||||
} // builtins.listToAttrs (builtins.map
|
|
||||||
(user: {
|
|
||||||
name = "${user.username}";
|
|
||||||
value = {
|
|
||||||
isNormalUser = true;
|
isNormalUser = true;
|
||||||
hashedPassword = user.hashedPassword;
|
hashedPassword = cfg.hashedMasterPassword;
|
||||||
openssh.authorizedKeys.keys = (if user ? sshKeys then user.sshKeys else [ ]);
|
openssh.authorizedKeys.keys = cfg.sshKeys;
|
||||||
};
|
};
|
||||||
})
|
}
|
||||||
cfg.users);
|
// builtins.listToAttrs (
|
||||||
|
builtins.map (user: {
|
||||||
|
name = "${user.username}";
|
||||||
|
value = {
|
||||||
|
isNormalUser = true;
|
||||||
|
hashedPassword = user.hashedPassword;
|
||||||
|
openssh.authorizedKeys.keys = (if user ? sshKeys then user.sshKeys else [ ]);
|
||||||
|
};
|
||||||
|
}) cfg.users
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -3,13 +3,13 @@ let
|
|||||||
cfg = config.selfprivacy;
|
cfg = config.selfprivacy;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
fileSystems = builtins.listToAttrs (builtins.map
|
fileSystems = builtins.listToAttrs (
|
||||||
(volume: {
|
builtins.map (volume: {
|
||||||
name = "${volume.mountPoint}";
|
name = "${volume.mountPoint}";
|
||||||
value = {
|
value = {
|
||||||
device = "${volume.device}";
|
device = "${volume.device}";
|
||||||
fsType = "${volume.fsType}";
|
fsType = "${volume.fsType}";
|
||||||
};
|
};
|
||||||
})
|
}) cfg.volumes
|
||||||
cfg.volumes);
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user