vuls: init at 0.27.0 (#348530)
[NixPkgs.git] / nixos / modules / services / web-apps / dependency-track.nix
blobba3dbee97288949acc6c7f35756d0dbc877da1e0
2   config,
3   pkgs,
4   lib,
5   ...
6 }:
7 let
8   cfg = config.services.dependency-track;
10   settingsFormat = pkgs.formats.javaProperties { };
12   frontendConfigFormat = pkgs.formats.json { };
13   frontendConfigFile = frontendConfigFormat.generate "config.json" {
14     API_BASE_URL = cfg.frontend.baseUrl;
15     OIDC_ISSUER = cfg.oidc.issuer;
16     OIDC_CLIENT_ID = cfg.oidc.clientId;
17     OIDC_SCOPE = cfg.oidc.scope;
18     OIDC_FLOW = cfg.oidc.flow;
19     OIDC_LOGIN_BUTTON_TEXT = cfg.oidc.loginButtonText;
20   };
22   sslEnabled =
23     config.services.nginx.virtualHosts.${cfg.nginx.domain}.addSSL
24     || config.services.nginx.virtualHosts.${cfg.nginx.domain}.forceSSL
25     || config.services.nginx.virtualHosts.${cfg.nginx.domain}.onlySSL
26     || config.services.nginx.virtualHosts.${cfg.nginx.domain}.enableACME;
28   assertStringPath =
29     optionName: value:
30     if lib.isPath value then
31       throw ''
32         services.dependency-track.${optionName}:
33           ${toString value}
34           is a Nix path, but should be a string, since Nix
35           paths are copied into the world-readable Nix store.
36       ''
37     else
38       value;
40   filterNull = lib.filterAttrs (_: v: v != null);
42   renderSettings =
43     settings:
44     lib.mapAttrs' (
45       n: v:
46       lib.nameValuePair (lib.toUpper (lib.replaceStrings [ "." ] [ "_" ] n)) (
47         if lib.isBool v then lib.boolToString v else v
48       )
49     ) (filterNull settings);
52   options.services.dependency-track = {
53     enable = lib.mkEnableOption "dependency-track";
55     package = lib.mkPackageOption pkgs "dependency-track" { };
57     logLevel = lib.mkOption {
58       type = lib.types.enum [
59         "INFO"
60         "WARN"
61         "ERROR"
62         "DEBUG"
63         "TRACE"
64       ];
65       default = "INFO";
66       description = "Log level for dependency-track";
67     };
69     port = lib.mkOption {
70       type = lib.types.port;
71       default = 8080;
72       description = ''
73         On which port dependency-track should listen for new HTTP connections.
74       '';
75     };
77     javaArgs = lib.mkOption {
78       type = lib.types.listOf lib.types.str;
79       default = [ "-Xmx4G" ];
80       description = "Java options passed to JVM";
81     };
83     database = {
84       type = lib.mkOption {
85         type = lib.types.enum [
86           "h2"
87           "postgresql"
88           "manual"
89         ];
90         default = "postgresql";
91         description = ''
92           `h2` database is not recommended for a production setup.
93           `postgresql` this settings it recommended for production setups.
94           `manual` the module doesn't handle database settings.
95         '';
96       };
98       createLocally = lib.mkOption {
99         type = lib.types.bool;
100         default = true;
101         description = ''
102           Whether a database should be automatically created on the
103           local host. Set this to false if you plan on provisioning a
104           local database yourself.
105         '';
106       };
108       databaseName = lib.mkOption {
109         type = lib.types.str;
110         default = "dependency-track";
111         description = ''
112           Database name to use when connecting to an external or
113           manually provisioned database; has no effect when a local
114           database is automatically provisioned.
116           To use this with a local database, set {option}`services.dependency-track.database.createLocally`
117           to `false` and create the database and user.
118         '';
119       };
121       username = lib.mkOption {
122         type = lib.types.str;
123         default = "dependency-track";
124         description = ''
125           Username to use when connecting to an external or manually
126           provisioned database; has no effect when a local database is
127           automatically provisioned.
129           To use this with a local database, set {option}`services.dependency-track.database.createLocally`
130           to `false` and create the database and user.
131         '';
132       };
134       passwordFile = lib.mkOption {
135         type = lib.types.path;
136         example = "/run/keys/db_password";
137         apply = assertStringPath "passwordFile";
138         description = ''
139           The path to a file containing the database password.
140         '';
141       };
142     };
144     ldap.bindPasswordFile = lib.mkOption {
145       type = lib.types.path;
146       example = "/run/keys/ldap_bind_password";
147       apply = assertStringPath "bindPasswordFile";
148       description = ''
149         The path to a file containing the LDAP bind password.
150       '';
151     };
153     frontend = {
154       baseUrl = lib.mkOption {
155         type = lib.types.str;
156         default = lib.optionalString cfg.nginx.enable "${
157           if sslEnabled then "https" else "http"
158         }://${cfg.nginx.domain}";
159         defaultText = lib.literalExpression ''
160           lib.optionalString config.services.dependency-track.nginx.enable "''${
161             if sslEnabled then "https" else "http"
162           }://''${config.services.dependency-track.nginx.domain}";
163         '';
164         description = ''
165           The base URL of the API server.
167           NOTE:
168           * This URL must be reachable by the browsers of your users.
169           * The frontend container itself does NOT communicate with the API server directly, it just serves static files.
170           * When deploying to dedicated servers, please use the external IP or domain of the API server.
171         '';
172       };
173     };
175     oidc = {
176       enable = lib.mkEnableOption "oidc support";
177       issuer = lib.mkOption {
178         type = lib.types.str;
179         default = "";
180         description = ''
181           Defines the issuer URL to be used for OpenID Connect.
182           See alpine.oidc.issuer property of the API server.
183         '';
184       };
185       clientId = lib.mkOption {
186         type = lib.types.str;
187         default = "";
188         description = ''
189           Defines the client ID for OpenID Connect.
190         '';
191       };
192       scope = lib.mkOption {
193         type = lib.types.str;
194         default = "openid profile email";
195         description = ''
196           Defines the scopes to request for OpenID Connect.
197           See also: https://openid.net/specs/openid-connect-basic-1_0.html#Scopes
198         '';
199       };
200       flow = lib.mkOption {
201         type = lib.types.enum [
202           "code"
203           "implicit"
204         ];
205         default = "code";
206         description = ''
207           Specifies the OpenID Connect flow to use.
208           Values other than "implicit" will result in the Code+PKCE flow to be used.
209           Usage of the implicit flow is strongly discouraged, but may be necessary when
210           the IdP of choice does not support the Code+PKCE flow.
211           See also:
212             - https://oauth.net/2/grant-types/implicit/
213             - https://oauth.net/2/pkce/
214         '';
215       };
216       loginButtonText = lib.mkOption {
217         type = lib.types.str;
218         default = "Login with OpenID Connect";
219         description = ''
220           Defines the scopes to request for OpenID Connect.
221           See also: https://openid.net/specs/openid-connect-basic-1_0.html#Scopes
222         '';
223       };
224       usernameClaim = lib.mkOption {
225         type = lib.types.str;
226         default = "name";
227         example = "preferred_username";
228         description = ''
229           Defines the name of the claim that contains the username in the provider's userinfo endpoint.
230           Common claims are "name", "username", "preferred_username" or "nickname".
231           See also: https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
232         '';
233       };
234       userProvisioning = lib.mkOption {
235         type = lib.types.bool;
236         default = false;
237         example = true;
238         description = ''
239           Specifies if mapped OpenID Connect accounts are automatically created upon successful
240           authentication. When a user logs in with a valid access token but an account has
241           not been previously provisioned, an authentication failure will be returned.
242           This allows admins to control specifically which OpenID Connect users can access the
243           system and which users cannot. When this value is set to true, a local OpenID Connect
244           user will be created and mapped to the OpenID Connect account automatically. This
245           automatic provisioning only affects authentication, not authorization.
246         '';
247       };
248       teamSynchronization = lib.mkOption {
249         type = lib.types.bool;
250         default = false;
251         example = true;
252         description = ''
253           This option will ensure that team memberships for OpenID Connect users are dynamic and
254           synchronized with membership of OpenID Connect groups or assigned roles. When a team is
255           mapped to an OpenID Connect group, all local OpenID Connect users will automatically be
256           assigned to the team if they are a member of the group the team is mapped to. If the user
257           is later removed from the OpenID Connect group, they will also be removed from the team. This
258           option provides the ability to dynamically control user permissions via the identity provider.
259           Note that team synchronization is only performed during user provisioning and after successful
260           authentication.
261         '';
262       };
263       teams = {
264         claim = lib.mkOption {
265           type = lib.types.str;
266           default = "groups";
267           description = ''
268             Defines the name of the claim that contains group memberships or role assignments in the provider's userinfo endpoint.
269             The claim must be an array of strings. Most public identity providers do not support group or role management.
270             When using a customizable / on-demand hosted identity provider, name, content, and inclusion in the userinfo endpoint
271             will most likely need to be configured.
272           '';
273         };
274         default = lib.mkOption {
275           type = lib.types.nullOr lib.types.commas;
276           default = null;
277           description = ''
278             Defines one or more team names that auto-provisioned OIDC users shall be added to.
279             Multiple team names may be provided as comma-separated list.
281             Has no effect when {option}`services.dependency-track.oidc.userProvisioning`=false,
282             or {option}`services.dependency-track.oidc.teamSynchronization`=true.
283           '';
284         };
285       };
286     };
288     nginx = {
289       enable = lib.mkOption {
290         type = lib.types.bool;
291         default = true;
292         example = false;
293         description = ''
294           Whether to set up an nginx virtual host.
295         '';
296       };
298       domain = lib.mkOption {
299         type = lib.types.str;
300         example = "dtrack.example.com";
301         description = ''
302           The domain name under which to set up the virtual host.
303         '';
304       };
305     };
307     settings = lib.mkOption {
308       type = lib.types.submodule {
309         freeformType = settingsFormat.type;
310         options = {
311           "alpine.data.directory" = lib.mkOption {
312             type = lib.types.path;
313             default = "/var/lib/dependency-track";
314             description = ''
315               Defines the path to the data directory. This directory will hold logs, keys,
316               and any database or index files along with application-specific files or
317               directories.
318             '';
319           };
320           "alpine.database.mode" = lib.mkOption {
321             type = lib.types.enum [
322               "server"
323               "embedded"
324               "external"
325             ];
326             default =
327               if cfg.database.type == "h2" then
328                 "embedded"
329               else if cfg.database.type == "postgresql" then
330                 "external"
331               else
332                 null;
333             defaultText = lib.literalExpression ''
334               if config.services.dependency-track.database.type == "h2" then "embedded"
335               else if config.services.dependency-track.database.type == "postgresql" then "external"
336               else null
337             '';
338             description = ''
339               Defines the database mode of operation. Valid choices are:
340               'server', 'embedded', and 'external'.
341               In server mode, the database will listen for connections from remote hosts.
342               In embedded mode, the system will be more secure and slightly faster.
343               External mode should be used when utilizing an external database server
344               (i.e. mysql, postgresql, etc).
345             '';
346           };
347           "alpine.database.url" = lib.mkOption {
348             type = lib.types.str;
349             default =
350               if cfg.database.type == "h2" then
351                 "jdbc:h2:/var/lib/dependency-track/db"
352               else if cfg.database.type == "postgresql" then
353                 "jdbc:postgresql:${cfg.database.databaseName}?socketFactory=org.newsclub.net.unix.AFUNIXSocketFactory$FactoryArg&socketFactoryArg=/run/postgresql/.s.PGSQL.5432"
354               else
355                 null;
357             defaultText = lib.literalExpression ''
358               if config.services.dependency-track.database.type == "h2" then "jdbc:h2:/var/lib/dependency-track/db"
359                 else if config.services.dependency-track.database.type == "postgresql" then "jdbc:postgresql:''${config.services.dependency-track.database.name}?socketFactory=org.newsclub.net.unix.AFUNIXSocketFactory$FactoryArg&socketFactoryArg=/run/postgresql/.s.PGSQL.5432"
360                 else null
361             '';
362             description = "Specifies the JDBC URL to use when connecting to the database.";
363           };
364           "alpine.database.driver" = lib.mkOption {
365             type = lib.types.enum [
366               "org.h2.Driver"
367               "org.postgresql.Driver"
368               "com.microsoft.sqlserver.jdbc.SQLServerDriver"
369               "com.mysql.cj.jdbc.Driver"
370             ];
371             default =
372               if cfg.database.type == "h2" then
373                 "org.h2.Driver"
374               else if cfg.database.type == "postgresql" then
375                 "org.postgresql.Driver"
376               else
377                 null;
378             defaultText = lib.literalExpression ''
379               if config.services.dependency-track.database.type == "h2" then "org.h2.Driver"
380               else if config.services.dependency-track.database.type == "postgresql" then "org.postgresql.Driver"
381               else null;
382             '';
383             description = "Specifies the JDBC driver class to use.";
384           };
385           "alpine.database.username" = lib.mkOption {
386             type = lib.types.str;
387             default = if cfg.database.createLocally then "dependency-track" else cfg.database.username;
388             defaultText = lib.literalExpression ''
389               if config.services.dependency-track.database.createLocally then "dependency-track"
390               else config.services.dependency-track.database.username
391             '';
392             description = "Specifies the username to use when authenticating to the database.";
393           };
394           "alpine.ldap.enabled" = lib.mkOption {
395             type = lib.types.bool;
396             default = false;
397             description = ''
398               Defines if LDAP will be used for user authentication. If enabled,
399               alpine.ldap.* properties should be set accordingly.
400             '';
401           };
402           "alpine.oidc.enabled" = lib.mkOption {
403             type = lib.types.bool;
404             default = cfg.oidc.enable;
405             defaultText = lib.literalExpression "config.services.dependency-track.oidc.enable";
406             description = ''
407               Defines if OpenID Connect will be used for user authentication.
408               If enabled, alpine.oidc.* properties should be set accordingly.
409             '';
410           };
411           "alpine.oidc.client.id" = lib.mkOption {
412             type = lib.types.str;
413             default = cfg.oidc.clientId;
414             defaultText = lib.literalExpression "config.services.dependency-track.oidc.clientId";
415             description = ''
416               Defines the client ID to be used for OpenID Connect.
417               The client ID should be the same as the one configured for the frontend,
418               and will only be used to validate ID tokens.
419             '';
420           };
421           "alpine.oidc.issuer" = lib.mkOption {
422             type = lib.types.str;
423             default = cfg.oidc.issuer;
424             defaultText = lib.literalExpression "config.services.dependency-track.oidc.issuer";
425             description = ''
426               Defines the issuer URL to be used for OpenID Connect.
427               This issuer MUST support provider configuration via the /.well-known/openid-configuration endpoint.
428               See also:
429               - https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
430               - https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig
431             '';
432           };
433           "alpine.oidc.username.claim" = lib.mkOption {
434             type = lib.types.str;
435             default = cfg.oidc.usernameClaim;
436             defaultText = lib.literalExpression "config.services.dependency-track.oidc.usernameClaim";
437             description = ''
438               Defines the name of the claim that contains the username in the provider's userinfo endpoint.
439               Common claims are "name", "username", "preferred_username" or "nickname".
440               See also: https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
441             '';
442           };
443           "alpine.oidc.user.provisioning" = lib.mkOption {
444             type = lib.types.bool;
445             default = cfg.oidc.userProvisioning;
446             defaultText = lib.literalExpression "config.services.dependency-track.oidc.userProvisioning";
447             description = ''
448               Specifies if mapped OpenID Connect accounts are automatically created upon successful
449               authentication. When a user logs in with a valid access token but an account has
450               not been previously provisioned, an authentication failure will be returned.
451               This allows admins to control specifically which OpenID Connect users can access the
452               system and which users cannot. When this value is set to true, a local OpenID Connect
453               user will be created and mapped to the OpenID Connect account automatically. This
454               automatic provisioning only affects authentication, not authorization.
455             '';
456           };
457           "alpine.oidc.team.synchronization" = lib.mkOption {
458             type = lib.types.bool;
459             default = cfg.oidc.teamSynchronization;
460             defaultText = lib.literalExpression "config.services.dependency-track.oidc.teamSynchronization";
461             description = ''
462               This option will ensure that team memberships for OpenID Connect users are dynamic and
463               synchronized with membership of OpenID Connect groups or assigned roles. When a team is
464               mapped to an OpenID Connect group, all local OpenID Connect users will automatically be
465               assigned to the team if they are a member of the group the team is mapped to. If the user
466               is later removed from the OpenID Connect group, they will also be removed from the team. This
467               option provides the ability to dynamically control user permissions via the identity provider.
468               Note that team synchronization is only performed during user provisioning and after successful
469               authentication.
470             '';
471           };
472           "alpine.oidc.teams.claim" = lib.mkOption {
473             type = lib.types.str;
474             default = cfg.oidc.teams.claim;
475             defaultText = lib.literalExpression "config.services.dependency-track.oidc.teams.claim";
476             description = ''
477               Defines the name of the claim that contains group memberships or role assignments in the provider's userinfo endpoint.
478               The claim must be an array of strings. Most public identity providers do not support group or role management.
479               When using a customizable / on-demand hosted identity provider, name, content, and inclusion in the userinfo endpoint
480               will most likely need to be configured.
481             '';
482           };
483           "alpine.oidc.teams.default" = lib.mkOption {
484             type = lib.types.nullOr lib.types.commas;
485             default = cfg.oidc.teams.default;
486             defaultText = lib.literalExpression "config.services.dependency-track.oidc.teams.default";
487             description = ''
488               Defines one or more team names that auto-provisioned OIDC users shall be added to.
489               Multiple team names may be provided as comma-separated list.
491               Has no effect when {option}`services.dependency-track.oidc.userProvisioning`=false,
492               or {option}`services.dependency-track.oidc.teamSynchronization`=true.
493             '';
494           };
495         };
496       };
497       default = { };
498       description = "See https://docs.dependencytrack.org/getting-started/configuration/#default-configuration for possible options";
499     };
500   };
502   config = lib.mkIf cfg.enable {
503     services.nginx = lib.mkIf cfg.nginx.enable {
504       enable = true;
505       recommendedGzipSettings = lib.mkDefault true;
506       recommendedOptimisation = lib.mkDefault true;
507       recommendedProxySettings = lib.mkDefault true;
508       recommendedTlsSettings = lib.mkDefault true;
509       upstreams.dependency-track.servers."localhost:${toString cfg.port}" = { };
510       virtualHosts.${cfg.nginx.domain} = {
511         locations = {
512           "/".proxyPass = "http://dependency-track";
513           "= /static/config.json".alias = frontendConfigFile;
514         };
515       };
516     };
518     systemd.services.dependency-track-postgresql-init = lib.mkIf cfg.database.createLocally {
519       after = [ "postgresql.service" ];
520       before = [ "dependency-track.service" ];
521       bindsTo = [ "postgresql.service" ];
522       path = [ config.services.postgresql.package ];
523       serviceConfig = {
524         Type = "oneshot";
525         RemainAfterExit = true;
526         User = "postgres";
527         Group = "postgres";
528         LoadCredential = [ "db_password:${cfg.database.passwordFile}" ];
529         PrivateTmp = true;
530       };
531       script = ''
532         set -eou pipefail
533         shopt -s inherit_errexit
535         # Read the password from the credentials directory and
536         # escape any single quotes by adding additional single
537         # quotes after them, following the rules laid out here:
538         # https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-CONSTANTS
539         db_password="$(<"$CREDENTIALS_DIRECTORY/db_password")"
540         db_password="''${db_password//\'/\'\'}"
542         echo "CREATE ROLE \"dependency-track\" WITH LOGIN PASSWORD '$db_password' CREATEDB" > /tmp/create_role.sql
543         psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='dependency-track'" | grep -q 1 || psql -tA --file="/tmp/create_role.sql"
544         psql -tAc "SELECT 1 FROM pg_database WHERE datname = 'dependency-track'" | grep -q 1 || psql -tAc 'CREATE DATABASE "dependency-track" OWNER "dependency-track"'
545       '';
546     };
548     services.postgresql.enable = lib.mkIf cfg.database.createLocally (lib.mkDefault true);
550     systemd.services."dependency-track" =
551       let
552         databaseServices =
553           if cfg.database.createLocally then
554             [
555               "dependency-track-postgresql-init.service"
556               "postgresql.service"
557             ]
558           else
559             [ ];
560       in
561       {
562         description = "Dependency Track";
563         wantedBy = [ "multi-user.target" ];
564         requires = databaseServices;
565         after = databaseServices;
566         # provide settings via env vars to allow overriding default settings.
567         environment = {
568           HOME = "%S/dependency-track";
569         } // renderSettings cfg.settings;
570         serviceConfig = {
571           User = "dependency-track";
572           Group = "dependency-track";
573           DynamicUser = true;
574           StateDirectory = "dependency-track";
575           LoadCredential =
576             [ "db_password:${cfg.database.passwordFile}" ]
577             ++ lib.optional cfg.settings."alpine.ldap.enabled"
578               "ldap_bind_password:${cfg.ldap.bindPasswordFile}";
579         };
580         script = ''
581           set -eou pipefail
582           shopt -s inherit_errexit
584           export ALPINE_DATABASE_PASSWORD_FILE="$CREDENTIALS_DIRECTORY/db_password"
585           ${lib.optionalString cfg.settings."alpine.ldap.enabled" ''
586             export ALPINE_LDAP_BIND_PASSWORD="$(<"$CREDENTIALS_DIRECTORY/ldap_bind_password")"
587           ''}
589           exec ${lib.getExe pkgs.jre_headless} ${
590             lib.escapeShellArgs (
591               cfg.javaArgs
592               ++ [
593                 "-DdependencyTrack.logging.level=${cfg.logLevel}"
594                 "-jar"
595                 "${cfg.package}/share/dependency-track/dependency-track.jar"
596                 "-port"
597                 "${toString cfg.port}"
598               ]
599             )
600           }
601         '';
602       };
603   };
605   meta = {
606     maintainers = lib.teams.cyberus.members;
607   };