Merge pull request #264584 from Ma27/drop-privacyidea
privacyidea: remove
This commit is contained in:
@@ -341,6 +341,8 @@
|
|||||||
|
|
||||||
- `service.borgmatic.settings.location` and `services.borgmatic.configurations.<name>.location` are deprecated, please move your options out of sections to the global scope.
|
- `service.borgmatic.settings.location` and `services.borgmatic.configurations.<name>.location` are deprecated, please move your options out of sections to the global scope.
|
||||||
|
|
||||||
|
- `privacyidea` (and the corresponding `privacyidea-ldap-proxy`) has been removed from nixpkgs because it has severely outdated dependencies that became unmaintainable with nixpkgs' python package-set.
|
||||||
|
|
||||||
- `dagger` was removed because using a package called `dagger` and packaging it from source violates their trademark policy.
|
- `dagger` was removed because using a package called `dagger` and packaging it from source violates their trademark policy.
|
||||||
|
|
||||||
- `win-virtio` package was renamed to `virtio-win` to be consistent with the upstream package name.
|
- `win-virtio` package was renamed to `virtio-win` to be consistent with the upstream package name.
|
||||||
|
|||||||
@@ -1176,7 +1176,6 @@
|
|||||||
./services/security/opensnitch.nix
|
./services/security/opensnitch.nix
|
||||||
./services/security/pass-secret-service.nix
|
./services/security/pass-secret-service.nix
|
||||||
./services/security/physlock.nix
|
./services/security/physlock.nix
|
||||||
./services/security/privacyidea.nix
|
|
||||||
./services/security/shibboleth-sp.nix
|
./services/security/shibboleth-sp.nix
|
||||||
./services/security/sks.nix
|
./services/security/sks.nix
|
||||||
./services/security/sshguard.nix
|
./services/security/sshguard.nix
|
||||||
|
|||||||
@@ -1,458 +0,0 @@
|
|||||||
{ config, lib, options, pkgs, ... }:
|
|
||||||
|
|
||||||
with lib;
|
|
||||||
|
|
||||||
let
|
|
||||||
cfg = config.services.privacyidea;
|
|
||||||
opt = options.services.privacyidea;
|
|
||||||
|
|
||||||
uwsgi = pkgs.uwsgi.override { plugins = [ "python3" ]; python3 = pkgs.python310; };
|
|
||||||
python = uwsgi.python3;
|
|
||||||
penv = python.withPackages (const [ pkgs.privacyidea ]);
|
|
||||||
logCfg = pkgs.writeText "privacyidea-log.cfg" ''
|
|
||||||
[formatters]
|
|
||||||
keys=detail
|
|
||||||
|
|
||||||
[handlers]
|
|
||||||
keys=stream
|
|
||||||
|
|
||||||
[formatter_detail]
|
|
||||||
class=privacyidea.lib.log.SecureFormatter
|
|
||||||
format=[%(asctime)s][%(process)d][%(thread)d][%(levelname)s][%(name)s:%(lineno)d] %(message)s
|
|
||||||
|
|
||||||
[handler_stream]
|
|
||||||
class=StreamHandler
|
|
||||||
level=NOTSET
|
|
||||||
formatter=detail
|
|
||||||
args=(sys.stdout,)
|
|
||||||
|
|
||||||
[loggers]
|
|
||||||
keys=root,privacyidea
|
|
||||||
|
|
||||||
[logger_privacyidea]
|
|
||||||
handlers=stream
|
|
||||||
qualname=privacyidea
|
|
||||||
level=INFO
|
|
||||||
|
|
||||||
[logger_root]
|
|
||||||
handlers=stream
|
|
||||||
level=ERROR
|
|
||||||
'';
|
|
||||||
|
|
||||||
piCfgFile = pkgs.writeText "privacyidea.cfg" ''
|
|
||||||
SUPERUSER_REALM = [ '${concatStringsSep "', '" cfg.superuserRealm}' ]
|
|
||||||
SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2:///privacyidea'
|
|
||||||
SECRET_KEY = '${cfg.secretKey}'
|
|
||||||
PI_PEPPER = '${cfg.pepper}'
|
|
||||||
PI_ENCFILE = '${cfg.encFile}'
|
|
||||||
PI_AUDIT_KEY_PRIVATE = '${cfg.auditKeyPrivate}'
|
|
||||||
PI_AUDIT_KEY_PUBLIC = '${cfg.auditKeyPublic}'
|
|
||||||
PI_LOGCONFIG = '${logCfg}'
|
|
||||||
${cfg.extraConfig}
|
|
||||||
'';
|
|
||||||
|
|
||||||
renderValue = x:
|
|
||||||
if isList x then concatMapStringsSep "," (x: ''"${x}"'') x
|
|
||||||
else if isString x && hasInfix "," x then ''"${x}"''
|
|
||||||
else x;
|
|
||||||
|
|
||||||
ldapProxyConfig = pkgs.writeText "ldap-proxy.ini"
|
|
||||||
(generators.toINI {}
|
|
||||||
(flip mapAttrs cfg.ldap-proxy.settings
|
|
||||||
(const (mapAttrs (const renderValue)))));
|
|
||||||
|
|
||||||
privacyidea-token-janitor = pkgs.writeShellScriptBin "privacyidea-token-janitor" ''
|
|
||||||
exec -a privacyidea-token-janitor \
|
|
||||||
/run/wrappers/bin/sudo -u ${cfg.user} \
|
|
||||||
env PRIVACYIDEA_CONFIGFILE=${cfg.stateDir}/privacyidea.cfg \
|
|
||||||
${penv}/bin/privacyidea-token-janitor $@
|
|
||||||
'';
|
|
||||||
in
|
|
||||||
|
|
||||||
{
|
|
||||||
options = {
|
|
||||||
services.privacyidea = {
|
|
||||||
enable = mkEnableOption (lib.mdDoc "PrivacyIDEA");
|
|
||||||
|
|
||||||
environmentFile = mkOption {
|
|
||||||
type = types.nullOr types.path;
|
|
||||||
default = null;
|
|
||||||
example = "/root/privacyidea.env";
|
|
||||||
description = lib.mdDoc ''
|
|
||||||
File to load as environment file. Environment variables
|
|
||||||
from this file will be interpolated into the config file
|
|
||||||
using `envsubst` which is helpful for specifying
|
|
||||||
secrets:
|
|
||||||
```
|
|
||||||
{ services.privacyidea.secretKey = "$SECRET"; }
|
|
||||||
```
|
|
||||||
|
|
||||||
The environment-file can now specify the actual secret key:
|
|
||||||
```
|
|
||||||
SECRET=veryverytopsecret
|
|
||||||
```
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
stateDir = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = "/var/lib/privacyidea";
|
|
||||||
description = lib.mdDoc ''
|
|
||||||
Directory where all PrivacyIDEA files will be placed by default.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
superuserRealm = mkOption {
|
|
||||||
type = types.listOf types.str;
|
|
||||||
default = [ "super" "administrators" ];
|
|
||||||
description = lib.mdDoc ''
|
|
||||||
The realm where users are allowed to login as administrators.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
secretKey = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
example = "t0p s3cr3t";
|
|
||||||
description = lib.mdDoc ''
|
|
||||||
This is used to encrypt the auth_token.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
pepper = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
example = "Never know...";
|
|
||||||
description = lib.mdDoc ''
|
|
||||||
This is used to encrypt the admin passwords.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
encFile = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = "${cfg.stateDir}/enckey";
|
|
||||||
defaultText = literalExpression ''"''${config.${opt.stateDir}}/enckey"'';
|
|
||||||
description = lib.mdDoc ''
|
|
||||||
This is used to encrypt the token data and token passwords
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
auditKeyPrivate = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = "${cfg.stateDir}/private.pem";
|
|
||||||
defaultText = literalExpression ''"''${config.${opt.stateDir}}/private.pem"'';
|
|
||||||
description = lib.mdDoc ''
|
|
||||||
Private Key for signing the audit log.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
auditKeyPublic = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = "${cfg.stateDir}/public.pem";
|
|
||||||
defaultText = literalExpression ''"''${config.${opt.stateDir}}/public.pem"'';
|
|
||||||
description = lib.mdDoc ''
|
|
||||||
Public key for checking signatures of the audit log.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
adminPasswordFile = mkOption {
|
|
||||||
type = types.path;
|
|
||||||
description = lib.mdDoc "File containing password for the admin user";
|
|
||||||
};
|
|
||||||
|
|
||||||
adminEmail = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
example = "admin@example.com";
|
|
||||||
description = lib.mdDoc "Mail address for the admin user";
|
|
||||||
};
|
|
||||||
|
|
||||||
extraConfig = mkOption {
|
|
||||||
type = types.lines;
|
|
||||||
default = "";
|
|
||||||
description = lib.mdDoc ''
|
|
||||||
Extra configuration options for pi.cfg.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
user = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = "privacyidea";
|
|
||||||
description = lib.mdDoc "User account under which PrivacyIDEA runs.";
|
|
||||||
};
|
|
||||||
|
|
||||||
group = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = "privacyidea";
|
|
||||||
description = lib.mdDoc "Group account under which PrivacyIDEA runs.";
|
|
||||||
};
|
|
||||||
|
|
||||||
tokenjanitor = {
|
|
||||||
enable = mkEnableOption (lib.mdDoc "automatic runs of the token janitor");
|
|
||||||
interval = mkOption {
|
|
||||||
default = "quarterly";
|
|
||||||
type = types.str;
|
|
||||||
description = lib.mdDoc ''
|
|
||||||
Interval in which the cleanup program is supposed to run.
|
|
||||||
See {manpage}`systemd.time(7)` for further information.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
action = mkOption {
|
|
||||||
type = types.enum [ "delete" "mark" "disable" "unassign" ];
|
|
||||||
description = lib.mdDoc ''
|
|
||||||
Which action to take for matching tokens.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
unassigned = mkOption {
|
|
||||||
default = false;
|
|
||||||
type = types.bool;
|
|
||||||
description = lib.mdDoc ''
|
|
||||||
Whether to search for **unassigned** tokens
|
|
||||||
and apply [](#opt-services.privacyidea.tokenjanitor.action)
|
|
||||||
onto them.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
orphaned = mkOption {
|
|
||||||
default = true;
|
|
||||||
type = types.bool;
|
|
||||||
description = lib.mdDoc ''
|
|
||||||
Whether to search for **orphaned** tokens
|
|
||||||
and apply [](#opt-services.privacyidea.tokenjanitor.action)
|
|
||||||
onto them.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
ldap-proxy = {
|
|
||||||
enable = mkEnableOption (lib.mdDoc "PrivacyIDEA LDAP Proxy");
|
|
||||||
|
|
||||||
configFile = mkOption {
|
|
||||||
type = types.nullOr types.path;
|
|
||||||
default = null;
|
|
||||||
description = lib.mdDoc ''
|
|
||||||
Path to PrivacyIDEA LDAP Proxy configuration (proxy.ini).
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
user = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = "pi-ldap-proxy";
|
|
||||||
description = lib.mdDoc "User account under which PrivacyIDEA LDAP proxy runs.";
|
|
||||||
};
|
|
||||||
|
|
||||||
group = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = "pi-ldap-proxy";
|
|
||||||
description = lib.mdDoc "Group account under which PrivacyIDEA LDAP proxy runs.";
|
|
||||||
};
|
|
||||||
|
|
||||||
settings = mkOption {
|
|
||||||
type = with types; attrsOf (attrsOf (oneOf [ str bool int (listOf str) ]));
|
|
||||||
default = {};
|
|
||||||
description = lib.mdDoc ''
|
|
||||||
Attribute-set containing the settings for `privacyidea-ldap-proxy`.
|
|
||||||
It's possible to pass secrets using env-vars as substitutes and
|
|
||||||
use the option [](#opt-services.privacyidea.ldap-proxy.environmentFile)
|
|
||||||
to inject them via `envsubst`.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
environmentFile = mkOption {
|
|
||||||
default = null;
|
|
||||||
type = types.nullOr types.str;
|
|
||||||
description = lib.mdDoc ''
|
|
||||||
Environment file containing secrets to be substituted into
|
|
||||||
[](#opt-services.privacyidea.ldap-proxy.settings).
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = mkMerge [
|
|
||||||
|
|
||||||
(mkIf cfg.enable {
|
|
||||||
|
|
||||||
assertions = [
|
|
||||||
{
|
|
||||||
assertion = cfg.tokenjanitor.enable -> (cfg.tokenjanitor.orphaned || cfg.tokenjanitor.unassigned);
|
|
||||||
message = ''
|
|
||||||
privacyidea-token-janitor has no effect if neither orphaned nor unassigned tokens
|
|
||||||
are to be searched.
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
environment.systemPackages = [ pkgs.privacyidea (hiPrio privacyidea-token-janitor) ];
|
|
||||||
|
|
||||||
services.postgresql.enable = mkDefault true;
|
|
||||||
|
|
||||||
systemd.services.privacyidea-tokenjanitor = mkIf cfg.tokenjanitor.enable {
|
|
||||||
environment.PRIVACYIDEA_CONFIGFILE = "${cfg.stateDir}/privacyidea.cfg";
|
|
||||||
path = [ penv ];
|
|
||||||
serviceConfig = {
|
|
||||||
CapabilityBoundingSet = [ "" ];
|
|
||||||
ExecStart = "${pkgs.writeShellScript "pi-token-janitor" ''
|
|
||||||
${optionalString cfg.tokenjanitor.orphaned ''
|
|
||||||
echo >&2 "Removing orphaned tokens..."
|
|
||||||
privacyidea-token-janitor find \
|
|
||||||
--orphaned true \
|
|
||||||
--action ${cfg.tokenjanitor.action}
|
|
||||||
''}
|
|
||||||
${optionalString cfg.tokenjanitor.unassigned ''
|
|
||||||
echo >&2 "Removing unassigned tokens..."
|
|
||||||
privacyidea-token-janitor find \
|
|
||||||
--assigned false \
|
|
||||||
--action ${cfg.tokenjanitor.action}
|
|
||||||
''}
|
|
||||||
''}";
|
|
||||||
Group = cfg.group;
|
|
||||||
LockPersonality = true;
|
|
||||||
MemoryDenyWriteExecute = true;
|
|
||||||
ProtectHome = true;
|
|
||||||
ProtectHostname = true;
|
|
||||||
ProtectKernelLogs = true;
|
|
||||||
ProtectKernelModules = true;
|
|
||||||
ProtectKernelTunables = true;
|
|
||||||
ProtectSystem = "strict";
|
|
||||||
ReadWritePaths = cfg.stateDir;
|
|
||||||
Type = "oneshot";
|
|
||||||
User = cfg.user;
|
|
||||||
WorkingDirectory = cfg.stateDir;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
systemd.timers.privacyidea-tokenjanitor = mkIf cfg.tokenjanitor.enable {
|
|
||||||
wantedBy = [ "timers.target" ];
|
|
||||||
timerConfig.OnCalendar = cfg.tokenjanitor.interval;
|
|
||||||
timerConfig.Persistent = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.privacyidea = let
|
|
||||||
piuwsgi = pkgs.writeText "uwsgi.json" (builtins.toJSON {
|
|
||||||
uwsgi = {
|
|
||||||
buffer-size = 8192;
|
|
||||||
plugins = [ "python3" ];
|
|
||||||
pythonpath = "${penv}/${uwsgi.python3.sitePackages}";
|
|
||||||
socket = "/run/privacyidea/socket";
|
|
||||||
uid = cfg.user;
|
|
||||||
gid = cfg.group;
|
|
||||||
chmod-socket = 770;
|
|
||||||
chown-socket = "${cfg.user}:nginx";
|
|
||||||
chdir = cfg.stateDir;
|
|
||||||
wsgi-file = "${penv}/etc/privacyidea/privacyideaapp.wsgi";
|
|
||||||
processes = 4;
|
|
||||||
harakiri = 60;
|
|
||||||
reload-mercy = 8;
|
|
||||||
stats = "/run/privacyidea/stats.socket";
|
|
||||||
max-requests = 2000;
|
|
||||||
limit-as = 1024;
|
|
||||||
reload-on-as = 512;
|
|
||||||
reload-on-rss = 256;
|
|
||||||
no-orphans = true;
|
|
||||||
vacuum = true;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
in {
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
after = [ "postgresql.service" ];
|
|
||||||
path = with pkgs; [ openssl ];
|
|
||||||
environment.PRIVACYIDEA_CONFIGFILE = "${cfg.stateDir}/privacyidea.cfg";
|
|
||||||
preStart = let
|
|
||||||
pi-manage = "${config.security.sudo.package}/bin/sudo -u privacyidea -HE ${penv}/bin/pi-manage";
|
|
||||||
pgsu = config.services.postgresql.superUser;
|
|
||||||
psql = config.services.postgresql.package;
|
|
||||||
in ''
|
|
||||||
mkdir -p ${cfg.stateDir} /run/privacyidea
|
|
||||||
chown ${cfg.user}:${cfg.group} -R ${cfg.stateDir} /run/privacyidea
|
|
||||||
umask 077
|
|
||||||
${lib.getBin pkgs.envsubst}/bin/envsubst -o ${cfg.stateDir}/privacyidea.cfg \
|
|
||||||
-i "${piCfgFile}"
|
|
||||||
chown ${cfg.user}:${cfg.group} ${cfg.stateDir}/privacyidea.cfg
|
|
||||||
if ! test -e "${cfg.stateDir}/db-created"; then
|
|
||||||
${config.security.sudo.package}/bin/sudo -u ${pgsu} ${psql}/bin/createuser --no-superuser --no-createdb --no-createrole ${cfg.user}
|
|
||||||
${config.security.sudo.package}/bin/sudo -u ${pgsu} ${psql}/bin/createdb --owner ${cfg.user} privacyidea
|
|
||||||
${pi-manage} create_enckey
|
|
||||||
${pi-manage} create_audit_keys
|
|
||||||
${pi-manage} createdb
|
|
||||||
${pi-manage} admin add admin -e ${cfg.adminEmail} -p "$(cat ${cfg.adminPasswordFile})"
|
|
||||||
${pi-manage} db stamp head -d ${penv}/lib/privacyidea/migrations
|
|
||||||
touch "${cfg.stateDir}/db-created"
|
|
||||||
chmod g+r "${cfg.stateDir}/enckey" "${cfg.stateDir}/private.pem"
|
|
||||||
fi
|
|
||||||
${pi-manage} db upgrade -d ${penv}/lib/privacyidea/migrations
|
|
||||||
'';
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "notify";
|
|
||||||
ExecStart = "${uwsgi}/bin/uwsgi --json ${piuwsgi}";
|
|
||||||
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
|
|
||||||
EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
|
|
||||||
ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID";
|
|
||||||
NotifyAccess = "main";
|
|
||||||
KillSignal = "SIGQUIT";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
users.users.privacyidea = mkIf (cfg.user == "privacyidea") {
|
|
||||||
group = cfg.group;
|
|
||||||
isSystemUser = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
users.groups.privacyidea = mkIf (cfg.group == "privacyidea") {};
|
|
||||||
})
|
|
||||||
|
|
||||||
(mkIf cfg.ldap-proxy.enable {
|
|
||||||
|
|
||||||
assertions = [
|
|
||||||
{ assertion = let
|
|
||||||
xor = a: b: a && !b || !a && b;
|
|
||||||
in xor (cfg.ldap-proxy.settings == {}) (cfg.ldap-proxy.configFile == null);
|
|
||||||
message = "configFile & settings are mutually exclusive for services.privacyidea.ldap-proxy!";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
warnings = mkIf (cfg.ldap-proxy.configFile != null) [
|
|
||||||
"Using services.privacyidea.ldap-proxy.configFile is deprecated! Use the RFC42-style settings option instead!"
|
|
||||||
];
|
|
||||||
|
|
||||||
systemd.services.privacyidea-ldap-proxy = let
|
|
||||||
ldap-proxy-env = pkgs.python3.withPackages (ps: [ ps.privacyidea-ldap-proxy ]);
|
|
||||||
in {
|
|
||||||
description = "privacyIDEA LDAP proxy";
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
serviceConfig = {
|
|
||||||
User = cfg.ldap-proxy.user;
|
|
||||||
Group = cfg.ldap-proxy.group;
|
|
||||||
StateDirectory = "privacyidea-ldap-proxy";
|
|
||||||
EnvironmentFile = mkIf (cfg.ldap-proxy.environmentFile != null)
|
|
||||||
[ cfg.ldap-proxy.environmentFile ];
|
|
||||||
ExecStartPre =
|
|
||||||
"${pkgs.writeShellScript "substitute-secrets-ldap-proxy" ''
|
|
||||||
umask 0077
|
|
||||||
${pkgs.envsubst}/bin/envsubst \
|
|
||||||
-i ${ldapProxyConfig} \
|
|
||||||
-o $STATE_DIRECTORY/ldap-proxy.ini
|
|
||||||
''}";
|
|
||||||
ExecStart = let
|
|
||||||
configPath = if cfg.ldap-proxy.settings != {}
|
|
||||||
then "%S/privacyidea-ldap-proxy/ldap-proxy.ini"
|
|
||||||
else cfg.ldap-proxy.configFile;
|
|
||||||
in ''
|
|
||||||
${ldap-proxy-env}/bin/twistd \
|
|
||||||
--nodaemon \
|
|
||||||
--pidfile= \
|
|
||||||
-u ${cfg.ldap-proxy.user} \
|
|
||||||
-g ${cfg.ldap-proxy.group} \
|
|
||||||
ldap-proxy \
|
|
||||||
-c ${configPath}
|
|
||||||
'';
|
|
||||||
Restart = "always";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
users.users.pi-ldap-proxy = mkIf (cfg.ldap-proxy.user == "pi-ldap-proxy") {
|
|
||||||
group = cfg.ldap-proxy.group;
|
|
||||||
isSystemUser = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
users.groups.pi-ldap-proxy = mkIf (cfg.ldap-proxy.group == "pi-ldap-proxy") {};
|
|
||||||
})
|
|
||||||
];
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -685,7 +685,6 @@ in {
|
|||||||
predictable-interface-names = handleTest ./predictable-interface-names.nix {};
|
predictable-interface-names = handleTest ./predictable-interface-names.nix {};
|
||||||
printing-socket = handleTest ./printing.nix { socket = true; };
|
printing-socket = handleTest ./printing.nix { socket = true; };
|
||||||
printing-service = handleTest ./printing.nix { socket = false; };
|
printing-service = handleTest ./printing.nix { socket = false; };
|
||||||
privacyidea = handleTest ./privacyidea.nix {};
|
|
||||||
privoxy = handleTest ./privoxy.nix {};
|
privoxy = handleTest ./privoxy.nix {};
|
||||||
prometheus = handleTest ./prometheus.nix {};
|
prometheus = handleTest ./prometheus.nix {};
|
||||||
prometheus-exporters = handleTest ./prometheus-exporters.nix {};
|
prometheus-exporters = handleTest ./prometheus-exporters.nix {};
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
# Miscellaneous small tests that don't warrant their own VM run.
|
|
||||||
|
|
||||||
import ./make-test-python.nix ({ pkgs, ...} : rec {
|
|
||||||
name = "privacyidea";
|
|
||||||
meta = with pkgs.lib.maintainers; {
|
|
||||||
maintainers = [ ];
|
|
||||||
};
|
|
||||||
|
|
||||||
nodes.machine = { ... }: {
|
|
||||||
virtualisation.cores = 2;
|
|
||||||
|
|
||||||
services.privacyidea = {
|
|
||||||
enable = true;
|
|
||||||
secretKey = "$SECRET_KEY";
|
|
||||||
pepper = "$PEPPER";
|
|
||||||
adminPasswordFile = pkgs.writeText "admin-password" "testing";
|
|
||||||
adminEmail = "root@localhost";
|
|
||||||
|
|
||||||
# Don't try this at home!
|
|
||||||
environmentFile = pkgs.writeText "pi-secrets.env" ''
|
|
||||||
SECRET_KEY=testing
|
|
||||||
PEPPER=testing
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
services.nginx = {
|
|
||||||
enable = true;
|
|
||||||
virtualHosts."_".locations."/".extraConfig = ''
|
|
||||||
uwsgi_pass unix:/run/privacyidea/socket;
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
testScript = ''
|
|
||||||
machine.start()
|
|
||||||
machine.wait_for_unit("multi-user.target")
|
|
||||||
machine.succeed("curl --fail http://localhost | grep privacyIDEA")
|
|
||||||
machine.succeed("grep \"SECRET_KEY = 'testing'\" /var/lib/privacyidea/privacyidea.cfg")
|
|
||||||
machine.succeed("grep \"PI_PEPPER = 'testing'\" /var/lib/privacyidea/privacyidea.cfg")
|
|
||||||
machine.succeed(
|
|
||||||
"curl --fail http://localhost/auth -F username=admin -F password=testing | grep token"
|
|
||||||
)
|
|
||||||
'';
|
|
||||||
})
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
{ lib, fetchFromGitHub, cacert, openssl, nixosTests
|
|
||||||
, python310, fetchPypi, fetchpatch
|
|
||||||
}:
|
|
||||||
|
|
||||||
let
|
|
||||||
dropDocOutput = { outputs, ... }: {
|
|
||||||
outputs = lib.filter (x: x != "doc") outputs;
|
|
||||||
};
|
|
||||||
|
|
||||||
# Follow issue below for Python 3.11 support
|
|
||||||
# https://github.com/privacyidea/privacyidea/issues/3593
|
|
||||||
python3' = python310.override {
|
|
||||||
packageOverrides = self: super: {
|
|
||||||
django = super.django_3;
|
|
||||||
|
|
||||||
sqlalchemy = super.sqlalchemy.overridePythonAttrs (oldAttrs: rec {
|
|
||||||
version = "1.3.24";
|
|
||||||
src = fetchPypi {
|
|
||||||
inherit (oldAttrs) pname;
|
|
||||||
inherit version;
|
|
||||||
hash = "sha256-67t3fL+TEjWbiXv4G6ANrg9ctp+6KhgmXcwYpvXvdRk=";
|
|
||||||
};
|
|
||||||
doCheck = false;
|
|
||||||
});
|
|
||||||
# version 3.3.0+ does not support SQLAlchemy 1.3
|
|
||||||
factory-boy = super.factory-boy.overridePythonAttrs (oldAttrs: rec {
|
|
||||||
version = "3.2.1";
|
|
||||||
src = oldAttrs.src.override {
|
|
||||||
inherit version;
|
|
||||||
hash = "sha256-qY0newwEfHXrbkq4UIp/gfsD0sshmG9ieRNUbveipV4=";
|
|
||||||
};
|
|
||||||
postPatch = "";
|
|
||||||
});
|
|
||||||
# fails with `no tests ran in 1.75s`
|
|
||||||
alembic = super.alembic.overridePythonAttrs (lib.const {
|
|
||||||
doCheck = false;
|
|
||||||
});
|
|
||||||
flask-migrate = super.flask-migrate.overridePythonAttrs (oldAttrs: rec {
|
|
||||||
version = "2.7.0";
|
|
||||||
src = fetchPypi {
|
|
||||||
pname = "Flask-Migrate";
|
|
||||||
inherit version;
|
|
||||||
hash = "sha256-ri8FZxWIdi3YOiHYsYxR/jVehng+JFlJlf+Nc4Df/jg=";
|
|
||||||
};
|
|
||||||
});
|
|
||||||
flask-sqlalchemy = super.flask-sqlalchemy.overridePythonAttrs (old: rec {
|
|
||||||
version = "2.5.1";
|
|
||||||
format = "setuptools";
|
|
||||||
src = fetchPypi {
|
|
||||||
pname = "Flask-SQLAlchemy";
|
|
||||||
inherit version;
|
|
||||||
hash = "sha256:2bda44b43e7cacb15d4e05ff3cc1f8bc97936cc464623424102bfc2c35e95912";
|
|
||||||
};
|
|
||||||
});
|
|
||||||
# Taken from by https://github.com/NixOS/nixpkgs/pull/173090/commits/d2c0c7eb4cc91beb0a1adbaf13abc0a526a21708
|
|
||||||
werkzeug = super.werkzeug.overridePythonAttrs (old: rec {
|
|
||||||
version = "1.0.1";
|
|
||||||
src = old.src.override {
|
|
||||||
inherit version;
|
|
||||||
hash = "sha256-bICx5a02ZSkOo5MguR4b4eDV9gZSuWSjBwIW3oPS5Hw=";
|
|
||||||
};
|
|
||||||
nativeCheckInputs = old.nativeCheckInputs ++ (with self; [
|
|
||||||
requests
|
|
||||||
]);
|
|
||||||
doCheck = false;
|
|
||||||
});
|
|
||||||
# Required by flask-1.1
|
|
||||||
jinja2 = super.jinja2.overridePythonAttrs (old: rec {
|
|
||||||
version = "2.11.3";
|
|
||||||
src = old.src.override {
|
|
||||||
inherit version;
|
|
||||||
hash = "sha256-ptWEM94K6AA0fKsfowQ867q+i6qdKeZo8cdoy4ejM8Y=";
|
|
||||||
};
|
|
||||||
patches = [
|
|
||||||
# python 3.10 compat fixes. In later upstream releases, but these
|
|
||||||
# are not compatible with flask 1 which we need here :(
|
|
||||||
(fetchpatch {
|
|
||||||
url = "https://github.com/thmo/jinja/commit/1efb4cc918b4f3d097c376596da101de9f76585a.patch";
|
|
||||||
hash = "sha256-GFaSvYxgzOEFmnnDIfcf0ImScNTh1lR4lxt2Uz1DYdU=";
|
|
||||||
})
|
|
||||||
(fetchpatch {
|
|
||||||
url = "https://github.com/mkrizek/jinja/commit/bd8bad37d1c0e2d8995a44fd88e234f5340afec5.patch";
|
|
||||||
hash = "sha256-Uow+gaO+/dH6zavC0X/SsuMAfhTLRWpamVlL87DXDRA=";
|
|
||||||
excludes = [ "CHANGES.rst" ];
|
|
||||||
})
|
|
||||||
];
|
|
||||||
});
|
|
||||||
# Required by jinja2-2.11.3
|
|
||||||
markupsafe = super.markupsafe.overridePythonAttrs (old: rec {
|
|
||||||
version = "2.0.1";
|
|
||||||
src = old.src.override {
|
|
||||||
inherit version;
|
|
||||||
hash = "sha256-WUxngH+xYjizDES99082wCzfItHIzake+KDtjav1Ygo=";
|
|
||||||
};
|
|
||||||
});
|
|
||||||
itsdangerous = super.itsdangerous.overridePythonAttrs (old: rec {
|
|
||||||
version = "1.1.0";
|
|
||||||
src = old.src.override {
|
|
||||||
inherit version;
|
|
||||||
hash = "sha256-MhsDPQfypBNtPsdi6snxahDM1g9TwMka+QIXrOe6Hxk=";
|
|
||||||
};
|
|
||||||
});
|
|
||||||
flask = super.flask.overridePythonAttrs (old: rec {
|
|
||||||
version = "1.1.4";
|
|
||||||
src = old.src.override {
|
|
||||||
inherit version;
|
|
||||||
hash = "sha256-D762GA04OpGG0NbtlU4AQq2fGODo3giLK0GdUmkn0ZY=";
|
|
||||||
};
|
|
||||||
});
|
|
||||||
sqlsoup = super.sqlsoup.overrideAttrs ({ meta ? {}, ... }: {
|
|
||||||
meta = meta // { broken = false; };
|
|
||||||
});
|
|
||||||
click = super.click.overridePythonAttrs (old: rec {
|
|
||||||
version = "7.1.2";
|
|
||||||
src = old.src.override {
|
|
||||||
inherit version;
|
|
||||||
hash = "sha256-0rUlXHxjSbwb0eWeCM0SrLvWPOZJ8liHVXg6qU37axo=";
|
|
||||||
};
|
|
||||||
disabledTests = [ "test_bytes_args" ]; # https://github.com/pallets/click/commit/6e05e1fa1c2804
|
|
||||||
});
|
|
||||||
# Now requires `lingua` as check input that requires a newer `click`,
|
|
||||||
# however `click-7` is needed by the older flask we need here. Since it's just
|
|
||||||
# for the test-suite apparently, let's skip it for now.
|
|
||||||
mako = super.mako.overridePythonAttrs (lib.const {
|
|
||||||
nativeCheckInputs = [];
|
|
||||||
doCheck = false;
|
|
||||||
});
|
|
||||||
# Requires pytest-httpserver as checkInput now which requires Werkzeug>=2 which is not
|
|
||||||
# supported by current privacyIDEA.
|
|
||||||
responses = super.responses.overridePythonAttrs (lib.const {
|
|
||||||
doCheck = false;
|
|
||||||
});
|
|
||||||
flask-babel = (super.flask-babel.override {
|
|
||||||
sphinxHook = null;
|
|
||||||
furo = null;
|
|
||||||
}).overridePythonAttrs (old: (dropDocOutput old) // rec {
|
|
||||||
pname = "Flask-Babel";
|
|
||||||
version = "2.0.0";
|
|
||||||
format = "setuptools";
|
|
||||||
src = fetchPypi {
|
|
||||||
inherit pname;
|
|
||||||
inherit version;
|
|
||||||
hash = "sha256:f9faf45cdb2e1a32ea2ec14403587d4295108f35017a7821a2b1acb8cfd9257d";
|
|
||||||
};
|
|
||||||
disabledTests = [
|
|
||||||
# AssertionError: assert 'Apr 12, 2010...46:00\u202fPM' == 'Apr 12, 2010, 1:46:00 PM'
|
|
||||||
# Note the `\u202f` (narrow, no-break space) vs space.
|
|
||||||
"test_basics"
|
|
||||||
"test_init_app"
|
|
||||||
"test_custom_locale_selector"
|
|
||||||
"test_refreshing"
|
|
||||||
];
|
|
||||||
});
|
|
||||||
psycopg2 = (super.psycopg2.override {
|
|
||||||
sphinxHook = null;
|
|
||||||
sphinx-better-theme = null;
|
|
||||||
}).overridePythonAttrs dropDocOutput;
|
|
||||||
pyjwt = (super.pyjwt.override {
|
|
||||||
sphinxHook = null;
|
|
||||||
sphinx-rtd-theme = null;
|
|
||||||
}).overridePythonAttrs (old: (dropDocOutput old) // { format = "setuptools"; });
|
|
||||||
beautifulsoup4 = (super.beautifulsoup4.override {
|
|
||||||
sphinxHook = null;
|
|
||||||
}).overridePythonAttrs dropDocOutput;
|
|
||||||
pydash = (super.pydash.override {
|
|
||||||
sphinx-rtd-theme = null;
|
|
||||||
}).overridePythonAttrs (old: rec {
|
|
||||||
version = "5.1.0";
|
|
||||||
src = fetchPypi {
|
|
||||||
inherit (old) pname;
|
|
||||||
inherit version;
|
|
||||||
hash = "sha256-GysFCsG64EnNB/WSCxT6u+UmOPSF2a2h6xFanuv/aDU=";
|
|
||||||
};
|
|
||||||
format = "setuptools";
|
|
||||||
doCheck = false;
|
|
||||||
});
|
|
||||||
pyopenssl = (super.pyopenssl.override {
|
|
||||||
sphinxHook = null;
|
|
||||||
sphinx-rtd-theme = null;
|
|
||||||
}).overridePythonAttrs dropDocOutput;
|
|
||||||
deprecated = (super.deprecated.override {
|
|
||||||
sphinxHook = null;
|
|
||||||
}).overridePythonAttrs dropDocOutput;
|
|
||||||
wrapt = (super.wrapt.override {
|
|
||||||
sphinxHook = null;
|
|
||||||
sphinx-rtd-theme = null;
|
|
||||||
}).overridePythonAttrs dropDocOutput;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in
|
|
||||||
python3'.pkgs.buildPythonPackage rec {
|
|
||||||
pname = "privacyIDEA";
|
|
||||||
version = "3.8.1";
|
|
||||||
format = "setuptools";
|
|
||||||
|
|
||||||
src = fetchFromGitHub {
|
|
||||||
owner = pname;
|
|
||||||
repo = pname;
|
|
||||||
rev = "v${version}";
|
|
||||||
hash = "sha256-SYXw8PBCb514v3rcy15W/vZS5JyMsu81D2sJmviLRtw=";
|
|
||||||
fetchSubmodules = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
patches = [
|
|
||||||
# https://github.com/privacyidea/privacyidea/pull/3611
|
|
||||||
(fetchpatch {
|
|
||||||
url = "https://github.com/privacyidea/privacyidea/commit/7db6509721726a34e8528437ddbd4210019b11ef.patch";
|
|
||||||
sha256 = "sha256-ZvtauCs1vWyxzGbA0B2+gG8q5JyUO8DF8nm/3/vcYmE=";
|
|
||||||
})
|
|
||||||
];
|
|
||||||
|
|
||||||
propagatedBuildInputs = with python3'.pkgs; [
|
|
||||||
cryptography pyrad pymysql python-dateutil flask-versioned flask-script
|
|
||||||
defusedxml croniter flask-migrate pyjwt configobj sqlsoup pillow
|
|
||||||
python-gnupg passlib pyopenssl beautifulsoup4 smpplib flask-babel
|
|
||||||
ldap3 huey pyyaml qrcode oauth2client requests lxml cbor2 psycopg2
|
|
||||||
pydash ecdsa google-auth importlib-metadata argon2-cffi bcrypt segno
|
|
||||||
];
|
|
||||||
|
|
||||||
passthru.tests = { inherit (nixosTests) privacyidea; };
|
|
||||||
|
|
||||||
nativeCheckInputs = with python3'.pkgs; [ openssl mock pytestCheckHook responses testfixtures ];
|
|
||||||
preCheck = "export HOME=$(mktemp -d)";
|
|
||||||
postCheck = "unset HOME";
|
|
||||||
disabledTests = [
|
|
||||||
# expects `/home/` to exist, fails with `FileNotFoundError: [Errno 2] No such file or directory: '/home/'`.
|
|
||||||
"test_01_loading_scripts"
|
|
||||||
|
|
||||||
# Tries to connect to `fcm.googleapis.com`.
|
|
||||||
"test_02_api_push_poll"
|
|
||||||
"test_04_decline_auth_request"
|
|
||||||
|
|
||||||
# Timezone info not available in build sandbox
|
|
||||||
"test_14_convert_timestamp_to_utc"
|
|
||||||
|
|
||||||
# Fails because of different logger configurations
|
|
||||||
"test_01_create_default_app"
|
|
||||||
"test_03_logging_config_file"
|
|
||||||
"test_04_logging_config_yaml"
|
|
||||||
"test_05_logging_config_broken_yaml"
|
|
||||||
];
|
|
||||||
|
|
||||||
pythonImportsCheck = [ "privacyidea" ];
|
|
||||||
|
|
||||||
postPatch = ''
|
|
||||||
patchShebangs tests/testdata/scripts
|
|
||||||
substituteInPlace privacyidea/lib/resolvers/LDAPIdResolver.py --replace \
|
|
||||||
"/etc/privacyidea/ldap-ca.crt" \
|
|
||||||
"${cacert}/etc/ssl/certs/ca-bundle.crt"
|
|
||||||
'';
|
|
||||||
|
|
||||||
postInstall = ''
|
|
||||||
rm -r $out/${python3'.sitePackages}/tests
|
|
||||||
'';
|
|
||||||
|
|
||||||
meta = with lib; {
|
|
||||||
description = "Multi factor authentication system (2FA, MFA, OTP Server)";
|
|
||||||
license = licenses.agpl3Plus;
|
|
||||||
homepage = "http://www.privacyidea.org";
|
|
||||||
maintainers = with maintainers; [ ma27 ];
|
|
||||||
platforms = platforms.linux;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
{ lib, buildPythonPackage, fetchFromGitHub, twisted, ldaptor, configobj, fetchpatch }:
|
|
||||||
|
|
||||||
buildPythonPackage rec {
|
|
||||||
pname = "privacyidea-ldap-proxy";
|
|
||||||
version = "0.7";
|
|
||||||
|
|
||||||
src = fetchFromGitHub {
|
|
||||||
owner = "privacyidea";
|
|
||||||
repo = pname;
|
|
||||||
rev = "v${version}";
|
|
||||||
sha256 = "1i2kgxqd38xvb42qj0a4a35w4vk0fyp3n7w48kqmvrxc77p6r6i8";
|
|
||||||
};
|
|
||||||
|
|
||||||
patches = [
|
|
||||||
# support for LDAPCompareRequest.
|
|
||||||
(fetchpatch {
|
|
||||||
url = "https://github.com/mayflower/privacyidea-ldap-proxy/commit/a13356717379b174f1a6abf767faa0dbd459f5dd.patch";
|
|
||||||
hash = "sha256-SBTj9ayQ8JFD8BoYIl77nxWVV3PXnHZ8JMlJnxd/nEk=";
|
|
||||||
})
|
|
||||||
];
|
|
||||||
|
|
||||||
propagatedBuildInputs = [ twisted ldaptor configobj ];
|
|
||||||
|
|
||||||
pythonImportsCheck = [ "pi_ldapproxy" ];
|
|
||||||
|
|
||||||
meta = with lib; {
|
|
||||||
description = "LDAP Proxy to intercept LDAP binds and authenticate against privacyIDEA";
|
|
||||||
homepage = "https://github.com/privacyidea/privacyidea-ldap-proxy";
|
|
||||||
license = licenses.agpl3Only;
|
|
||||||
maintainers = [ ];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -710,6 +710,7 @@ mapAliases ({
|
|||||||
pinentry_qt = throw "'pinentry_qt' has been renamed to/replaced by 'pinentry-qt'"; # Converted to throw 2023-09-10
|
pinentry_qt = throw "'pinentry_qt' has been renamed to/replaced by 'pinentry-qt'"; # Converted to throw 2023-09-10
|
||||||
pinentry_qt5 = pinentry-qt; # Added 2020-02-11
|
pinentry_qt5 = pinentry-qt; # Added 2020-02-11
|
||||||
poetry2nix = throw "poetry2nix is now maintained out-of-tree. Please use https://github.com/nix-community/poetry2nix/"; # Added 2023-10-26
|
poetry2nix = throw "poetry2nix is now maintained out-of-tree. Please use https://github.com/nix-community/poetry2nix/"; # Added 2023-10-26
|
||||||
|
privacyidea = throw "privacyidea has been removed from nixpkgs"; # Added 2023-10-31
|
||||||
probe-rs-cli = throw "probe-rs-cli is now part of the probe-rs package"; # Added 2023-07-03
|
probe-rs-cli = throw "probe-rs-cli is now part of the probe-rs package"; # Added 2023-07-03
|
||||||
processing3 = throw "'processing3' has been renamed to/replaced by 'processing'"; # Converted to throw 2023-09-10
|
processing3 = throw "'processing3' has been renamed to/replaced by 'processing'"; # Converted to throw 2023-09-10
|
||||||
prometheus-dmarc-exporter = dmarc-metrics-exporter; # added 2022-05-31
|
prometheus-dmarc-exporter = dmarc-metrics-exporter; # added 2022-05-31
|
||||||
|
|||||||
@@ -19871,8 +19871,6 @@ with pkgs;
|
|||||||
|
|
||||||
premake = premake4;
|
premake = premake4;
|
||||||
|
|
||||||
privacyidea = callPackage ../applications/misc/privacyidea { };
|
|
||||||
|
|
||||||
process-compose = callPackage ../applications/misc/process-compose { };
|
process-compose = callPackage ../applications/misc/process-compose { };
|
||||||
|
|
||||||
process-viewer = callPackage ../applications/misc/process-viewer { };
|
process-viewer = callPackage ../applications/misc/process-viewer { };
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ mapAliases ({
|
|||||||
poster3 = throw "poster3 is unmaintained and source is no longer available"; # added 2023-05-29
|
poster3 = throw "poster3 is unmaintained and source is no longer available"; # added 2023-05-29
|
||||||
postorius = throw "Please use pkgs.mailmanPackages.postorius"; # added 2022-04-29
|
postorius = throw "Please use pkgs.mailmanPackages.postorius"; # added 2022-04-29
|
||||||
powerlineMemSegment = powerline-mem-segment; # added 2021-10-08
|
powerlineMemSegment = powerline-mem-segment; # added 2021-10-08
|
||||||
privacyidea = throw "privacyidea has been renamed to pkgs.privacyidea"; # added 2021-06-20
|
privacyidea-ldap-proxy = throw "privacyidea-ldap-proxy has been removed from nixpkgs"; # added 2023-10-31
|
||||||
prometheus_client = prometheus-client; # added 2021-06-10
|
prometheus_client = prometheus-client; # added 2021-06-10
|
||||||
prompt_toolkit = prompt-toolkit; # added 2021-07-22
|
prompt_toolkit = prompt-toolkit; # added 2021-07-22
|
||||||
protonup = protonup-ng; # Added 2022-11-06
|
protonup = protonup-ng; # Added 2022-11-06
|
||||||
|
|||||||
@@ -9510,8 +9510,6 @@ self: super: with self; {
|
|||||||
|
|
||||||
prison = callPackage ../development/python-modules/prison { };
|
prison = callPackage ../development/python-modules/prison { };
|
||||||
|
|
||||||
privacyidea-ldap-proxy = callPackage ../development/python-modules/privacyidea-ldap-proxy { };
|
|
||||||
|
|
||||||
proboscis = callPackage ../development/python-modules/proboscis { };
|
proboscis = callPackage ../development/python-modules/proboscis { };
|
||||||
|
|
||||||
process-tests = callPackage ../development/python-modules/process-tests { };
|
process-tests = callPackage ../development/python-modules/process-tests { };
|
||||||
|
|||||||
Reference in New Issue
Block a user