indico: wip: add LDAP support

Signed-off-by: Jeltz <jeltz@federez.net>
This commit is contained in:
jeltz 2025-02-22 12:47:58 +01:00
parent 0a8ae58334
commit dd2afc2cfb
Signed by: jeltz
GPG key ID: 800882B66C0C3326
2 changed files with 143 additions and 29 deletions

View file

@ -14,7 +14,9 @@ let
pythonEnv = pkgs.python3.withPackages (ps: [ pythonEnv = pkgs.python3.withPackages (ps: [
(ps.toPythonModule indico) (ps.toPythonModule indico)
ps.gunicorn ps.gunicorn
]); ]
++ lib.optionals (cfg.ldap != null)
indico.optional-dependencies.ldap);
redisSocket = config.services.redis.servers.${cfg.redis.name}.unixSocket; redisSocket = config.services.redis.servers.${cfg.redis.name}.unixSocket;
indicoSocket = "/run/indico/indico.sock"; indicoSocket = "/run/indico/indico.sock";
baseDir = "${pythonEnv}/${pythonEnv.sitePackages}/indico"; baseDir = "${pythonEnv}/${pythonEnv.sitePackages}/indico";
@ -36,7 +38,23 @@ let
format = "%(levelname)s %(request_id)s %(user_id)s %(name)s %(message)s"; format = "%(levelname)s %(request_id)s %(user_id)s %(name)s %(message)s";
}; };
}; };
configFile = pythonFmt.generate "indico.conf" { ldapConfig = {
uri = cfg.ldap.uri;
bind_dn = cfg.ldap.bindDN;
bind_password = cfg.ldap.bindPassword;
timeout = 30;
verify_cert = true;
page_size = 1500;
uid = cfg.ldap.uid;
user_base = cfg.ldap.userBaseDN;
user_filter = cfg.ldap.userFilter;
gid = cfg.ldap.gid;
group_base = cfg.ldap.groupBaseDN;
group_filter = cfg.ldap.groupFilter;
member_of_attr = cfg.ldap.memberOf;
ad_group_style = false;
};
configFile = pythonFmt.generate "indico.conf" ({
SQLALCHEMY_DATABASE_URI = cfg.database; SQLALCHEMY_DATABASE_URI = cfg.database;
CACHE_DIR = "${cfg.stateDir}/cache"; CACHE_DIR = "${cfg.stateDir}/cache";
TEMP_DIR = "${cfg.stateDir}/tmp"; TEMP_DIR = "${cfg.stateDir}/tmp";
@ -64,9 +82,27 @@ let
SMTP_LOGIN = cfg.email.smtp.login; SMTP_LOGIN = cfg.email.smtp.login;
SMTP_PASSWORD = cfg.email.smtp.password; SMTP_PASSWORD = cfg.email.smtp.password;
SMTP_USE_TLS = cfg.email.smtp.useTLS; SMTP_USE_TLS = cfg.email.smtp.useTLS;
}; } // lib.optionalAttrs (cfg.ldap != null) {
in AUTH_PROVIDERS = {
{ ldap = {
type = "ldap";
title = "LDAP";
ldap = ldapConfig;
default = true;
};
};
IDENTITY_PROVIDERS = {
ldap = {
type = "ldap";
title = "LDAP";
ldap = ldapConfig;
mapping = cfg.ldap.mapping;
trusted_email = cfg.ldap.trustedEmail;
synced_fields = cfg.ldap.syncedFields;
};
};
});
in {
# TODO cProfile; indico standalone command is *very* slow # TODO cProfile; indico standalone command is *very* slow
# (~30s just to print the help) # (~30s just to print the help)
@ -235,16 +271,101 @@ in
"postgresql://@/${cfg.user}?host=/run/postgresql"; "postgresql://@/${cfg.user}?host=/run/postgresql";
description = "Database URL."; description = "Database URL.";
}; };
ldap = lib.mkOption {
type = lib.types.nullOr (lib.types.submodule {
options = {
uri = lib.mkOption {
type = lib.types.str;
description = "LDAP server URI.";
};
bindDN = lib.mkOption {
type = lib.types.str;
description = "LDAP server bind DN.";
};
bindPassword = lib.mkOption {
type = lib.types.str;
description = "LDAP server bind password.";
};
uid = lib.mkOption {
type = lib.types.str;
default = "uid";
description = "LDAP UID attribute.";
};
userBaseDN = lib.mkOption {
type = lib.types.str;
description = "LDAP users base DN.";
};
userFilter = lib.mkOption {
type = lib.types.str;
description = "LDAP users filter.";
};
gid = lib.mkOption {
type = lib.types.str;
default = "gid";
description = "LDAP GID attribute.";
};
groupBaseDN = lib.mkOption {
type = lib.types.str;
description = "LDAP groups base DN.";
};
groupFilter = lib.mkOption {
type = lib.types.str;
description = "LDAP groups filter.";
};
memberOf = lib.mkOption {
type = lib.types.str;
default = "memberOf";
description = "LDAP memberOf attribute.";
};
mapping = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = {
first_name = "givenName";
last_name = "sn";
email = "mail";
affiliation = "company";
phone = "telephoneNumber";
};
description = "Mapping between local and LDAP fields.";
};
trustedEmail = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether LDAP emails are to be trusted.";
};
syncedFields = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "first_name" "last_name" ];
description = "Fields to sync with LDAP server.";
};
};
});
default = null;
description = "LDAP authentication and identity provider configuration.";
};
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
users.users = { users.users = {
${cfg.user} = { ${cfg.user} = {
isSystemUser = true; isSystemUser = true;
group = cfg.group; group = cfg.group;
}; };
} // (lib.optionalAttrs cfg.nginx.enable { } // (lib.optionalAttrs cfg.nginx.enable {
${config.services.nginx.user}.extraGroups = [ cfg.group ]; ${config.services.nginx.user}.extraGroups = [ cfg.group ];
}); });
users.groups.${cfg.group} = { }; users.groups.${cfg.group} = { };
@ -254,12 +375,10 @@ in
package = pkgs.postgresql_16; # TODO add setting package = pkgs.postgresql_16; # TODO add setting
# TODO db name != cfg.user # TODO db name != cfg.user
ensureDatabases = [ cfg.user ]; ensureDatabases = [ cfg.user ];
ensureUsers = [ ensureUsers = [{
{ name = cfg.user;
name = cfg.user; ensureDBOwnership = true;
ensureDBOwnership = true; }];
}
];
}; };
environment.systemPackages = environment.systemPackages =
@ -268,7 +387,7 @@ in
#!${pkgs.runtimeShell} #!${pkgs.runtimeShell}
sudo=exec sudo=exec
if [[ "$USER" != "indico" ]]; then if [[ "$USER" != "indico" ]]; then
sudo='exec /run/wrappers/bin/sudo -u indico -E INDICO_CONFIG' sudo='exec /run/wrappers/bin/sudo -u indico --preserve-env=INDICO_CONFIG'
fi fi
export INDICO_CONFIG=${configFile} export INDICO_CONFIG=${configFile}
$sudo ${lib.getExe' pythonEnv "indico"} "$@" $sudo ${lib.getExe' pythonEnv "indico"} "$@"
@ -311,9 +430,7 @@ in
{ {
indico-db = lib.recursiveUpdate common { indico-db = lib.recursiveUpdate common {
description = "Indico database preparation and upgrade"; description = "Indico database preparation and upgrade";
after = [ after = [ "postgresql.service" ];
"postgresql.service"
];
serviceConfig.Type = "oneshot"; serviceConfig.Type = "oneshot";
# Source: pretalx module ; passer par un service oneshot # Source: pretalx module ; passer par un service oneshot
script = '' script = ''
@ -356,13 +473,13 @@ in
"indico-worker.service" "indico-worker.service"
"indico-db.service" "indico-db.service"
]; ];
# TODO bind on a TCP socket when cfg.nginx.enable == false? # TODO bind on a TCP socket when cfg.nginx.enable == false?
serviceConfig.ExecStart = '' serviceConfig.ExecStart = ''
${lib.getExe' pythonEnv "gunicorn"} \ ${lib.getExe' pythonEnv "gunicorn"} \
--bind unix:${indicoSocket} \ --bind unix:${indicoSocket} \
--name=indico \ --name=indico \
indico.web.wsgi indico.web.wsgi
''; '';
}; };
}; };

View file

@ -69,7 +69,6 @@ python.pkgs.buildPythonApplication rec {
INDICO_NO_GIT = "true"; INDICO_NO_GIT = "true";
INDICO_MAP_VERSION = src.outputHash; INDICO_MAP_VERSION = src.outputHash;
# TODO bin/maintenance/build-{wheel,assets}.py
build-system = [ build-system = [
python.pkgs.hatchling python.pkgs.hatchling
python.pkgs.hatch-requirements-txt python.pkgs.hatch-requirements-txt
@ -97,7 +96,7 @@ python.pkgs.buildPythonApplication rec {
# FIXME *why* is it required? # FIXME *why* is it required?
makeCacheWritable = true; makeCacheWritable = true;
npmFlags = [ "--verbose" ]; # npmFlags = [ "--verbose" ];
patches = [ patches = [
./remove_marshmallow_enum.patch ./remove_marshmallow_enum.patch
@ -137,11 +136,9 @@ python.pkgs.buildPythonApplication rec {
# TODO TODO ./bin/maintenance/build-assets.py # TODO TODO ./bin/maintenance/build-assets.py
''; '';
# TODO build-assets.py
# build_urls_map.py tries to import indico.web.flask.app # build_urls_map.py tries to import indico.web.flask.app
# which has not been installed yet at this stage # which has not been installed yet at this stage
preBuild = '' preBuild = ''
ls -lah
PYTHONPATH="$(pwd):$PYTHONPATH" ${lib.getExe python} bin/maintenance/build-assets.py indico --clean PYTHONPATH="$(pwd):$PYTHONPATH" ${lib.getExe python} bin/maintenance/build-assets.py indico --clean
''; '';
@ -233,9 +230,9 @@ python.pkgs.buildPythonApplication rec {
++ sentry-sdk_1.optional-dependencies.pure_eval ++ sentry-sdk_1.optional-dependencies.pure_eval
++ wtforms.optional-dependencies.email; ++ wtforms.optional-dependencies.email;
# passthru = { optional-dependencies = {
# inherit python; ldap = [ python.pkgs.python-ldap ];
# }; };
meta = { meta = {
description = "Full-featured conferency lifecycle management and meeting/lecture scheduling tool"; description = "Full-featured conferency lifecycle management and meeting/lecture scheduling tool";