diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 40904ef0c17..5adbc26522c 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -792,6 +792,7 @@ ./services/security/nginx-sso.nix ./services/security/oauth2_proxy.nix ./services/security/oauth2_proxy_nginx.nix + ./services/security/privacyidea.nix ./services/security/physlock.nix ./services/security/shibboleth-sp.nix ./services/security/sks.nix diff --git a/nixos/modules/services/security/privacyidea.nix b/nixos/modules/services/security/privacyidea.nix new file mode 100644 index 00000000000..d6abfd0e271 --- /dev/null +++ b/nixos/modules/services/security/privacyidea.nix @@ -0,0 +1,279 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.privacyidea; + + uwsgi = pkgs.uwsgi.override { plugins = [ "python3" ]; }; + python = uwsgi.python3; + penv = python.withPackages (ps: [ ps.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:///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} + ''; + +in + +{ + options = { + services.privacyidea = { + enable = mkEnableOption "PrivacyIDEA"; + + stateDir = mkOption { + type = types.str; + default = "/var/lib/privacyidea"; + description = '' + Directory where all PrivacyIDEA files will be placed by default. + ''; + }; + + superuserRealm = mkOption { + type = types.listOf types.str; + default = [ "super" "administrators" ]; + description = '' + The realm where users are allowed to login as administrators. + ''; + }; + + secretKey = mkOption { + type = types.str; + example = "t0p s3cr3t"; + description = '' + This is used to encrypt the auth_token. + ''; + }; + + pepper = mkOption { + type = types.str; + example = "Never know..."; + description = '' + This is used to encrypt the admin passwords. + ''; + }; + + encFile = mkOption { + type = types.str; + default = "${cfg.stateDir}/enckey"; + description = '' + This is used to encrypt the token data and token passwords + ''; + }; + + auditKeyPrivate = mkOption { + type = types.str; + default = "${cfg.stateDir}/private.pem"; + description = '' + Private Key for signing the audit log. + ''; + }; + + auditKeyPublic = mkOption { + type = types.str; + default = "${cfg.stateDir}/public.pem"; + description = '' + Public key for checking signatures of the audit log. + ''; + }; + + adminPasswordFile = mkOption { + type = types.path; + description = "File containing password for the admin user"; + }; + + adminEmail = mkOption { + type = types.str; + example = "admin@example.com"; + description = "Mail address for the admin user"; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra configuration options for pi.cfg. + ''; + }; + + user = mkOption { + type = types.str; + default = "privacyidea"; + description = "User account under which PrivacyIDEA runs."; + }; + + group = mkOption { + type = types.str; + default = "privacyidea"; + description = "Group account under which PrivacyIDEA runs."; + }; + + ldap-proxy = { + enable = mkEnableOption "PrivacyIDEA LDAP Proxy"; + + configFile = mkOption { + type = types.path; + default = ""; + description = '' + Path to PrivacyIDEA LDAP Proxy configuration (proxy.ini). + ''; + }; + + user = mkOption { + type = types.str; + default = "pi-ldap-proxy"; + description = "User account under which PrivacyIDEA LDAP proxy runs."; + }; + + group = mkOption { + type = types.str; + default = "pi-ldap-proxy"; + description = "Group account under which PrivacyIDEA LDAP proxy runs."; + }; + }; + }; + }; + + config = mkMerge [ + + (mkIf cfg.enable { + + environment.systemPackages = [ python.pkgs.privacyidea ]; + + services.postgresql.enable = mkDefault true; + + systemd.services.privacyidea = let + piuwsgi = pkgs.writeText "uwsgi.json" (builtins.toJSON { + uwsgi = { + 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 = piCfgFile; + preStart = let + pi-manage = "${pkgs.sudo}/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 + if ! test -e "${cfg.stateDir}/db-created"; then + ${pkgs.sudo}/bin/sudo -u ${pgsu} ${psql}/bin/createuser --no-superuser --no-createdb --no-createrole ${cfg.user} + ${pkgs.sudo}/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"; + ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID"; + NotifyAccess = "main"; + KillSignal = "SIGQUIT"; + StandardError = "syslog"; + }; + }; + + users.users.privacyidea = mkIf (cfg.user == "privacyidea") { + group = cfg.group; + }; + + users.groups.privacyidea = mkIf (cfg.group == "privacyidea") {}; + }) + + (mkIf cfg.ldap-proxy.enable { + + systemd.services.privacyidea-ldap-proxy = let + ldap-proxy-env = pkgs.python2.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; + ExecStart = '' + ${ldap-proxy-env}/bin/twistd \ + --nodaemon \ + --pidfile= \ + -u ${cfg.ldap-proxy.user} \ + -g ${cfg.ldap-proxy.group} \ + ldap-proxy \ + -c ${cfg.ldap-proxy.configFile} + ''; + Restart = "always"; + }; + }; + + users.users.pi-ldap-proxy = mkIf (cfg.ldap-proxy.user == "pi-ldap-proxy") { + group = cfg.ldap-proxy.group; + }; + + users.groups.pi-ldap-proxy = mkIf (cfg.ldap-proxy.group == "pi-ldap-proxy") {}; + }) + ]; + +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 5a0c9d1afae..4190f70eeee 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -260,6 +260,7 @@ in pppd = handleTest ./pppd.nix {}; predictable-interface-names = handleTest ./predictable-interface-names.nix {}; printing = handleTest ./printing.nix {}; + privacyidea = handleTest ./privacyidea.nix {}; prometheus = handleTest ./prometheus.nix {}; prometheus-exporters = handleTest ./prometheus-exporters.nix {}; prosody = handleTest ./xmpp/prosody.nix {}; diff --git a/nixos/tests/privacyidea.nix b/nixos/tests/privacyidea.nix new file mode 100644 index 00000000000..45c7cd37c24 --- /dev/null +++ b/nixos/tests/privacyidea.nix @@ -0,0 +1,36 @@ +# Miscellaneous small tests that don't warrant their own VM run. + +import ./make-test-python.nix ({ pkgs, ...} : rec { + name = "privacyidea"; + meta = with pkgs.stdenv.lib.maintainers; { + maintainers = [ fpletz ]; + }; + + machine = { ... }: { + virtualisation.cores = 2; + virtualisation.memorySize = 512; + + services.privacyidea = { + enable = true; + secretKey = "testing"; + pepper = "testing"; + adminPasswordFile = pkgs.writeText "admin-password" "testing"; + adminEmail = "root@localhost"; + }; + 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( + "curl --fail http://localhost/auth -F username=admin -F password=testing | grep token" + ) + ''; +}) diff --git a/pkgs/development/python-modules/flask-versioned/default.nix b/pkgs/development/python-modules/flask-versioned/default.nix new file mode 100644 index 00000000000..3bd5ef98b32 --- /dev/null +++ b/pkgs/development/python-modules/flask-versioned/default.nix @@ -0,0 +1,23 @@ +{ stdenv, buildPythonPackage, fetchFromGitHub, flask }: + +buildPythonPackage rec { + pname = "Flask-Versioned"; + version = "0.9.4-20101221"; + + src = fetchFromGitHub { + owner = "pilt"; + repo = "flask-versioned"; + rev = "38046fb53a09060de437c90a5f7370a6b94ffc31"; # no tags + sha256 = "1wim9hvx7lxzfg35c0nc7p34j4vw9mzisgijlz4ibgykah4g1y37"; + }; + + propagatedBuildInputs = [ flask ]; + + meta = with stdenv.lib; { + description = "Flask plugin to rewrite file paths to add version info"; + homepage = "https://github.com/pilt/flask-versioned"; + license = licenses.bsd3; + maintainers = with maintainers; [ globin ]; + }; +} + diff --git a/pkgs/development/python-modules/huey/default.nix b/pkgs/development/python-modules/huey/default.nix new file mode 100644 index 00000000000..28a801de71a --- /dev/null +++ b/pkgs/development/python-modules/huey/default.nix @@ -0,0 +1,25 @@ +{ lib, buildPythonPackage, fetchFromGitHub, redis }: + +buildPythonPackage rec { + pname = "huey"; + version = "2.2.0"; + + src = fetchFromGitHub { + owner = "coleifer"; + repo = pname; + rev = version; + sha256 = "1hgic7qrmb1kxvfgf2qqiw39nqyknf17pjvli8jfzvd9mv7cb7hh"; + }; + + propagatedBuildInputs = [ redis ]; + + # connects to redis + doCheck = false; + + meta = with lib; { + description = "A little task queue for python"; + homepage = "https://github.com/coleifer/huey"; + license = licenses.mit; + maintainers = [ maintainers.globin ]; + }; +} diff --git a/pkgs/development/python-modules/privacyidea/default.nix b/pkgs/development/python-modules/privacyidea/default.nix new file mode 100644 index 00000000000..84cc4530b34 --- /dev/null +++ b/pkgs/development/python-modules/privacyidea/default.nix @@ -0,0 +1,51 @@ +{ lib, buildPythonPackage, fetchFromGitHub, cacert, openssl, python + +, 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 + +, mock, pytest, responses, testfixtures +}: + +buildPythonPackage rec { + pname = "privacyIDEA"; + version = "3.3"; + + src = fetchFromGitHub { + owner = pname; + repo = pname; + rev = "v${version}"; + sha256 = "188ki924dig899wlih45xfsm0s7mjkya56vii26bg02h91izrb4b"; + }; + + propagatedBuildInputs = [ + 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 + ]; + + checkInputs = [ openssl mock pytest responses testfixtures ]; + # issues with hardware token tests + doCheck = false; + + pythonImportsCheck = [ "privacyidea" ]; + + postPatch = '' + substituteInPlace privacyidea/lib/resolvers/LDAPIdResolver.py --replace \ + "/etc/privacyidea/ldap-ca.crt" \ + "${cacert}/etc/ssl/certs/ca-bundle.crt" + ''; + + postInstall = '' + rm -rf $out/${python.sitePackages}/tests + ''; + + meta = with lib; { + description = "Multi factor authentication system (2FA, MFA, OTP Server)"; + license = licenses.agpl3Plus; + homepage = "http://www.privacyidea.org"; + maintainers = [ maintainers.globin ]; + }; +} diff --git a/pkgs/development/python-modules/privacyidea/ldap-proxy.nix b/pkgs/development/python-modules/privacyidea/ldap-proxy.nix new file mode 100644 index 00000000000..ec87186a4fd --- /dev/null +++ b/pkgs/development/python-modules/privacyidea/ldap-proxy.nix @@ -0,0 +1,27 @@ +{ lib, buildPythonPackage, fetchFromGitHub, twisted, ldaptor, configobj }: + +buildPythonPackage rec { + pname = "privacyidea-ldap-proxy"; + version = "0.6.1"; + + src = fetchFromGitHub { + owner = "privacyidea"; + repo = pname; + rev = "v${version}"; + sha256 = "1kc1n9wr1a66xd5zvl6dq78xnkqkn5574jpzashc99pvm62dr24j"; + }; + + propagatedBuildInputs = [ twisted ldaptor configobj ]; + + # python 2 zope.interface test import path issues + doCheck = false; + + 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.agpl3; + maintainers = [ maintainers.globin ]; + }; +} diff --git a/pkgs/development/python-modules/pyrad/default.nix b/pkgs/development/python-modules/pyrad/default.nix new file mode 100644 index 00000000000..31cbf77d94e --- /dev/null +++ b/pkgs/development/python-modules/pyrad/default.nix @@ -0,0 +1,27 @@ +{ buildPythonPackage, fetchFromGitHub, lib, netaddr, six, nose }: + +buildPythonPackage rec { + pname = "pyrad"; + version = "2.3"; + + src = fetchFromGitHub { + owner = "pyradius"; + repo = pname; + rev = version; + sha256 = "0hy7999av47s8100afbhxfjb8phbmrqcv530xlvskndby4a8w94k"; + }; + + propagatedBuildInputs = [ netaddr six ]; + checkInputs = [ nose ]; + + checkPhase = '' + nosetests -e testBind + ''; + + meta = with lib; { + description = "Python RADIUS Implementation"; + homepage = "https://bitbucket.org/zzzeek/sqlsoup"; + license = licenses.mit; + maintainers = [ maintainers.globin ]; + }; +} diff --git a/pkgs/development/python-modules/smpplib/default.nix b/pkgs/development/python-modules/smpplib/default.nix new file mode 100644 index 00000000000..fe22f121efb --- /dev/null +++ b/pkgs/development/python-modules/smpplib/default.nix @@ -0,0 +1,29 @@ +{ buildPythonPackage, fetchPypi, lib, python, six, tox, mock, pytest }: + +buildPythonPackage rec { + pname = "smpplib"; + version = "2.1.0"; + + src = fetchPypi { + inherit pname version; + sha256 = "0jzxlfwf0861ilh4xyd70hmkdbvdki52aalglm1bnpxkg6i3jhfz"; + }; + + propagatedBuildInputs = [ six ]; + checkInputs = [ tox mock pytest ]; + + checkPhase = '' + pytest + ''; + + postInstall = '' + rm -rf $out/${python.sitePackages}/tests + ''; + + meta = with lib; { + description = "SMPP library for Python"; + homepage = "https://github.com/python-smpplib/python-smpplib"; + license = licenses.lgpl3Plus; + maintainers = [ maintainers.globin ]; + }; +} diff --git a/pkgs/development/python-modules/sqlsoup/default.nix b/pkgs/development/python-modules/sqlsoup/default.nix new file mode 100644 index 00000000000..a6ffd3227cd --- /dev/null +++ b/pkgs/development/python-modules/sqlsoup/default.nix @@ -0,0 +1,21 @@ +{ buildPythonPackage, fetchPypi, lib, sqlalchemy, nose }: + +buildPythonPackage rec { + pname = "sqlsoup"; + version = "0.9.1"; + + src = fetchPypi { + inherit pname version; + sha256 = "1mj00fhxj75ac3i8xk9jmm7hvcjz9p4x2r3yndcwsgb659rvgbrg"; + }; + + propagatedBuildInputs = [ sqlalchemy ]; + checkInputs = [ nose ]; + + meta = with lib; { + description = "A one step database access tool, built on the SQLAlchemy ORM"; + homepage = "https://bitbucket.org/zzzeek/sqlsoup"; + license = licenses.mit; + maintainers = [ maintainers.globin ]; + }; +} diff --git a/pkgs/top-level/python-packages.nix b/pkgs/top-level/python-packages.nix index fec08dae0a7..513b5876bf0 100644 --- a/pkgs/top-level/python-packages.nix +++ b/pkgs/top-level/python-packages.nix @@ -2933,6 +2933,8 @@ in { hglib = callPackage ../development/python-modules/hglib {}; + huey = callPackage ../development/python-modules/huey { }; + humanize = callPackage ../development/python-modules/humanize { }; humanfriendly = callPackage ../development/python-modules/humanfriendly { }; @@ -3233,6 +3235,9 @@ in { priority = callPackage ../development/python-modules/priority { }; + privacyidea = callPackage ../development/python-modules/privacyidea { }; + privacyidea-ldap-proxy = callPackage ../development/python-modules/privacyidea/ldap-proxy.nix { }; + prov = callPackage ../development/python-modules/prov { }; pudb = callPackage ../development/python-modules/pudb { }; @@ -3753,6 +3758,8 @@ in { flask_testing = callPackage ../development/python-modules/flask-testing { }; + flask-versioned = callPackage ../development/python-modules/flask-versioned { }; + flask_wtf = callPackage ../development/python-modules/flask-wtf { }; wtforms = callPackage ../development/python-modules/wtforms { }; @@ -5229,6 +5236,8 @@ in { pyrabbit2 = callPackage ../development/python-modules/pyrabbit2 { }; + pyrad = callPackage ../development/python-modules/pyrad { }; + pyrr = callPackage ../development/python-modules/pyrr { }; pysha3 = callPackage ../development/python-modules/pysha3 { }; @@ -5839,6 +5848,8 @@ in { sqlalchemy-utils = callPackage ../development/python-modules/sqlalchemy-utils { }; + sqlsoup = callPackage ../development/python-modules/sqlsoup { }; + staticjinja = callPackage ../development/python-modules/staticjinja { }; statsmodels = callPackage ../development/python-modules/statsmodels { }; @@ -6251,6 +6262,8 @@ in { smartdc = callPackage ../development/python-modules/smartdc { }; + smpplib = callPackage ../development/python-modules/smpplib { }; + socksipy-branch = callPackage ../development/python-modules/socksipy-branch { }; sockjs-tornado = callPackage ../development/python-modules/sockjs-tornado { };