python3Packages.orjson: Disable failing tests on 32 bit
[NixPkgs.git] / nixos / modules / services / misc / portunus.nix
blob0b283ea27d82c18df59a07e1655e1967a2273487
1 { config, lib, pkgs, ... }:
3 with lib;
5 let
6   cfg = config.services.portunus;
8 in
10   options.services.portunus = {
11     enable = mkEnableOption (lib.mdDoc "Portunus, a self-contained user/group management and authentication service for LDAP");
13     domain = mkOption {
14       type = types.str;
15       example = "sso.example.com";
16       description = lib.mdDoc "Subdomain which gets reverse proxied to Portunus webserver.";
17     };
19     port = mkOption {
20       type = types.port;
21       default = 8080;
22       description = lib.mdDoc ''
23         Port where the Portunus webserver should listen on.
25         This must be put behind a TLS-capable reverse proxy because Portunus only listens on localhost.
26       '';
27     };
29     package = mkOption {
30       type = types.package;
31       default = pkgs.portunus;
32       defaultText = "pkgs.portunus";
33       description = lib.mdDoc "The Portunus package to use.";
34     };
36     seedPath = mkOption {
37       type = types.nullOr types.path;
38       default = null;
39       description = lib.mdDoc ''
40         Path to a portunus seed file in json format.
41         See <https://github.com/majewsky/portunus#seeding-users-and-groups-from-static-configuration> for available options.
42       '';
43     };
45     stateDir = mkOption {
46       type = types.path;
47       default = "/var/lib/portunus";
48       description = lib.mdDoc "Path where Portunus stores its state.";
49     };
51     user = mkOption {
52       type = types.str;
53       default = "portunus";
54       description = lib.mdDoc "User account under which Portunus runs its webserver.";
55     };
57     group = mkOption {
58       type = types.str;
59       default = "portunus";
60       description = lib.mdDoc "Group account under which Portunus runs its webserver.";
61     };
63     dex = {
64       enable = mkEnableOption (lib.mdDoc ''
65         Dex ldap connector.
67         To activate dex, first a search user must be created in the Portunus web ui
68         and then the password must to be set as the `DEX_SEARCH_USER_PASSWORD` environment variable
69         in the [](#opt-services.dex.environmentFile) setting.
70       '');
72       oidcClients = mkOption {
73         type = types.listOf (types.submodule {
74           options = {
75             callbackURL = mkOption {
76               type = types.str;
77               description = lib.mdDoc "URL where the OIDC client should redirect";
78             };
79             id = mkOption {
80               type = types.str;
81               description = lib.mdDoc "ID of the OIDC client";
82             };
83           };
84         });
85         default = [ ];
86         example = [
87           {
88             callbackURL = "https://example.com/client/oidc/callback";
89             id = "service";
90           }
91         ];
92         description = lib.mdDoc ''
93           List of OIDC clients.
95           The OIDC secret must be set as the `DEX_CLIENT_''${id}` environment variable
96           in the [](#opt-services.dex.environmentFile) setting.
97         '';
98       };
100       port = mkOption {
101         type = types.port;
102         default = 5556;
103         description = lib.mdDoc "Port where dex should listen on.";
104       };
105     };
107     ldap = {
108       package = mkOption {
109         type = types.package;
110         default = pkgs.openldap;
111         defaultText = "pkgs.openldap";
112         description = lib.mdDoc "The OpenLDAP package to use.";
113       };
115       searchUserName = mkOption {
116         type = types.str;
117         default = "";
118         example = "admin";
119         description = lib.mdDoc ''
120           The login name of the search user.
121           This user account must be configured in Portunus either manually or via seeding.
122         '';
123       };
125       suffix = mkOption {
126         type = types.str;
127         example = "dc=example,dc=org";
128         description = lib.mdDoc ''
129           The DN of the topmost entry in your LDAP directory.
130           Please refer to the Portunus documentation for more information on how this impacts the structure of the LDAP directory.
131         '';
132       };
134       tls = mkOption {
135         type = types.bool;
136         default = false;
137         description = lib.mdDoc ''
138           Wether to enable LDAPS protocol.
139           This also adds two entries to the `/etc/hosts` file to point [](#opt-services.portunus.domain) to localhost,
140           so that CLIs and programs can use ldaps protocol and verify the certificate without opening the firewall port for the protocol.
142           This requires a TLS certificate for [](#opt-services.portunus.domain) to be configured via [](#opt-security.acme.certs).
143         '';
144       };
146       user = mkOption {
147         type = types.str;
148         default = "openldap";
149         description = lib.mdDoc "User account under which Portunus runs its LDAP server.";
150       };
152       group = mkOption {
153         type = types.str;
154         default = "openldap";
155         description = lib.mdDoc "Group account under which Portunus runs its LDAP server.";
156       };
157     };
158   };
160   config = mkIf cfg.enable {
161     assertions = [
162       {
163         assertion = cfg.dex.enable -> cfg.ldap.searchUserName != "";
164         message = "services.portunus.dex.enable requires services.portunus.ldap.searchUserName to be set.";
165       }
166     ];
168     # add ldapsearch(1) etc. to interactive shells
169     environment.systemPackages = [ cfg.ldap.package ];
171     # allow connecting via ldaps /w certificate without opening ports
172     networking.hosts = mkIf cfg.ldap.tls {
173       "::1" = [ cfg.domain ];
174       "127.0.0.1" = [ cfg.domain ];
175     };
177     services.dex = mkIf cfg.dex.enable {
178       enable = true;
179       settings = {
180         issuer = "https://${cfg.domain}/dex";
181         web.http = "127.0.0.1:${toString cfg.dex.port}";
182         storage = {
183           type = "sqlite3";
184           config.file = "/var/lib/dex/dex.db";
185         };
186         enablePasswordDB = false;
187         connectors = [{
188           type = "ldap";
189           id = "ldap";
190           name = "LDAP";
191           config = {
192             host = "${cfg.domain}:636";
193             bindDN = "uid=${cfg.ldap.searchUserName},ou=users,${cfg.ldap.suffix}";
194             bindPW = "$DEX_SEARCH_USER_PASSWORD";
195             userSearch = {
196               baseDN = "ou=users,${cfg.ldap.suffix}";
197               filter = "(objectclass=person)";
198               username = "uid";
199               idAttr = "uid";
200               emailAttr = "mail";
201               nameAttr = "cn";
202               preferredUsernameAttr = "uid";
203             };
204             groupSearch = {
205               baseDN = "ou=groups,${cfg.ldap.suffix}";
206               filter = "(objectclass=groupOfNames)";
207               nameAttr = "cn";
208               userMatchers = [{ userAttr = "DN"; groupAttr = "member"; }];
209             };
210           };
211         }];
213         staticClients = forEach cfg.dex.oidcClients (client: {
214           inherit (client) id;
215           redirectURIs = [ client.callbackURL ];
216           name = "OIDC for ${client.id}";
217           secretEnv = "DEX_CLIENT_${client.id}";
218         });
219       };
220     };
222     systemd.services = {
223       dex.serviceConfig = mkIf cfg.dex.enable {
224         # `dex.service` is super locked down out of the box, but we need some
225         # place to write the SQLite database. This creates $STATE_DIRECTORY below
226         # /var/lib/private because DynamicUser=true, but it gets symlinked into
227         # /var/lib/dex inside the unit
228         StateDirectory = "dex";
229       };
231       portunus = {
232         description = "Self-contained authentication service";
233         wantedBy = [ "multi-user.target" ];
234         after = [ "network.target" ];
235         serviceConfig.ExecStart = "${cfg.package.out}/bin/portunus-orchestrator";
236         environment = {
237           PORTUNUS_LDAP_SUFFIX = cfg.ldap.suffix;
238           PORTUNUS_SERVER_BINARY = "${cfg.package}/bin/portunus-server";
239           PORTUNUS_SERVER_GROUP = cfg.group;
240           PORTUNUS_SERVER_USER = cfg.user;
241           PORTUNUS_SERVER_HTTP_LISTEN = "[::]:${toString cfg.port}";
242           PORTUNUS_SERVER_STATE_DIR = cfg.stateDir;
243           PORTUNUS_SLAPD_BINARY = "${cfg.ldap.package}/libexec/slapd";
244           PORTUNUS_SLAPD_GROUP = cfg.ldap.group;
245           PORTUNUS_SLAPD_USER = cfg.ldap.user;
246           PORTUNUS_SLAPD_SCHEMA_DIR = "${cfg.ldap.package}/etc/schema";
247         } // (optionalAttrs (cfg.seedPath != null) ({
248           PORTUNUS_SEED_PATH = cfg.seedPath;
249         })) // (optionalAttrs cfg.ldap.tls (
250           let
251             acmeDirectory = config.security.acme.certs."${cfg.domain}".directory;
252           in
253           {
254             PORTUNUS_SLAPD_TLS_CA_CERTIFICATE = "/etc/ssl/certs/ca-certificates.crt";
255             PORTUNUS_SLAPD_TLS_CERTIFICATE = "${acmeDirectory}/cert.pem";
256             PORTUNUS_SLAPD_TLS_DOMAIN_NAME = cfg.domain;
257             PORTUNUS_SLAPD_TLS_PRIVATE_KEY = "${acmeDirectory}/key.pem";
258           }));
259       };
260     };
262     users.users = mkMerge [
263       (mkIf (cfg.ldap.user == "openldap") {
264         openldap = {
265           group = cfg.ldap.group;
266           isSystemUser = true;
267         };
268       })
269       (mkIf (cfg.user == "portunus") {
270         portunus = {
271           group = cfg.group;
272           isSystemUser = true;
273         };
274       })
275     ];
277     users.groups = mkMerge [
278       (mkIf (cfg.ldap.user == "openldap") {
279         openldap = { };
280       })
281       (mkIf (cfg.user == "portunus") {
282         portunus = { };
283       })
284     ];
285   };
287   meta.maintainers = [ maintainers.majewsky ] ++ teams.c3d2.members;