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