{ pkgs, config, lib, ... }: with lib; let cfg = config.services.bitwarden_directory_connector; ldap_data = builtins.toJSON cfg.ldap; sync_data = builtins.toJSON cfg.sync; in { imports = []; options.services.bitwarden_directory_connector = { enable = mkEnableOption "Bitwarden Directory Connector"; package = mkOption { type = types.package; default = pkgs.bitwarden-directory-connector; defaultText = literalExpression "pkgs.bitwarden-directory-connector"; description = lib.mdDoc "Reference to the Bitwarden Directory Connector package"; example = literalExpression "pkgs.bitwarden-directory-connector-example"; }; binary_name = mkOption { type = types.str; description = lib.mdDoc "The main binary for the connector."; default = "bitwarden-directory-connector"; }; domain = mkOption { type = types.str; description = lib.mdDoc "The domain the Bitwarden/Vaultwarden is accessable on."; example = "https://vaultwarden.example.com"; }; user = mkOption { type = types.str; description = lib.mdDoc "User to run the program."; default = "bwdc"; }; directory = mkOption { type = types.str; description = lib.mdDoc "Folder to store the config file."; default = "/etc/bitwarden/bwdc"; }; interval = mkOption { type = types.str; default = "*:0,15,30,45"; description = lib.mdDoc "When to run the connector, OnCalendar syntax."; }; ldap = mkOption { description = lib.mdDoc "Options to configurate LDAP."; type = types.submodule { freeformType = types.attrsOf (pkgs.formats.json {}).type; options = { ssl = mkOption { type = types.bool; default = false; description = lib.mdDoc "Use SSL."; }; startTls = mkOption { type = types.bool; default = false; description = lib.mdDoc "Use STARTTLS."; }; sslAllowUnauthorized = mkOption { type = types.bool; default = false; description = lib.mdDoc ""; }; port = mkOption { type = types.int; default = 389; description = lib.mdDoc "Port LDAP is accessable on"; }; currentUser = mkOption { type = types.bool; default = false; description = lib.mdDoc "Unknown what this does."; }; ad = mkOption { type = types.bool; default = false; description = lib.mdDoc "Is Active Directory."; }; pagedSearch = mkOption { type = types.bool; default = false; description = lib.mdDoc "The LDAP server paginates search results."; }; hostname = mkOption { type = types.str; description = lib.mdDoc "The host the LDAP is accessable on."; example = "ldap.example.com"; }; rootPath = mkOption { type = types.str; description = lib.mdDoc "Root path for LDAP"; example = "dc=example,dc=com"; }; username = mkOption { type = types.str; description = lib.mdDoc "The user to authenticate as."; example = "cn=admin,dc=example,dc=com"; }; }; }; }; sync = mkOption { description = lib.mdDoc "Options to configurate what gets synced."; type = types.submodule { freeformType = types.attrsOf (pkgs.formats.json {}).type; options = { removeDisabled = mkOption { type = types.bool; default = true; description = lib.mdDoc "Remove users from bitwarden groups if no longer in the ldap group."; }; overwriteExisting = mkOption { type = types.bool; default = false; description = lib.mdDoc "Remove and re-add users/groups, See https://bitwarden.com/help/user-group-filters/#overwriting-syncs for more details."; }; largeImport = mkOption { type = types.bool; default = false; description = lib.mdDoc "Enable if you are syncing more than 2000 users/groups."; }; memberAttribute = mkOption { type = types.str; description = lib.mdDoc "Attribute that lists members in a LDAP group."; example = "uniqueMember"; }; creationDateAttribute = mkOption { type = types.str; description = lib.mdDoc "Attribute that lists a users creation date."; example = "whenCreated"; }; useEmailPrefixSuffix = mkOption { type = types.bool; default = false; description = lib.mdDoc "If a user has no email address, combine a username prefix with a suffix value to form an email."; }; emailPrefixAttribute = mkOption { type = types.str; description = lib.mdDoc "Attribute that has a users username."; default = "accountName"; }; emailSuffix = mkOption { type = types.str; description = lib.mdDoc "Suffix for the email, normally @example.com."; default = "@example.com"; }; users = mkOption { type = types.bool; default = false; description = lib.mdDoc "Sync users."; }; userPath = mkOption { type = types.str; description = lib.mdDoc "User directory, relative to root."; default = "ou=users"; }; userObjectClass = mkOption { type = types.str; description = lib.mdDoc "A class that users will have."; default = "inetOrgPerson"; }; userEmailAttribute = mkOption { type = types.str; description = lib.mdDoc "Attribute for a users email."; default = "mail"; }; userFilter = mkOption { type = types.str; description = lib.mdDoc "Filter for users."; example = "(memberOf=cn=sales,ou=groups,dc=example,dc=com)"; default = ""; }; groups = mkOption { type = types.bool; default = false; description = lib.mdDoc "Sync groups."; }; groupPath = mkOption { type = types.str; description = lib.mdDoc "Group directory, relative to root."; default = "ou=groups"; }; groupObjectClass = mkOption { type = types.str; description = lib.mdDoc "A class that groups will have."; default = "groupOfNames"; }; groupNameAttribute = mkOption { type = types.str; description = lib.mdDoc "Attribute for a name of group."; default = "cn"; }; groupFilter = mkOption { type = types.str; description = lib.mdDoc "Filter for groups."; example = "(cn=sales)"; default = ""; }; }; }; }; secrets = { ldap = mkOption rec { type = types.str; description = "Auth for the LDAP, has value defined in {option}`pw_env"; }; bitwarden = { client_path_id = mkOption rec { type = types.str; description = "Path to file that contains Client ID."; }; client_path_secret = mkOption rec { type = types.str; description = "Path to file that contains Client Secret."; }; }; }; }; config = mkIf cfg.enable { users.groups."${cfg.user}" = {}; users.users."${cfg.user}" = { createHome = true; isSystemUser = true; home = "${cfg.directory}"; group = "${cfg.user}"; homeMode = "711"; }; systemd = { timers.bitwarden_directory_connector = { description = "Sync timer for Bitwarden Directory Connector"; wantedBy = ["timers.target"]; partOf = ["bitwarden_directory_connector.service"]; timerConfig = { OnCalendar = cfg.interval; Unit = "bitwarden_directory_connector.service"; Persistent = true; }; }; services.bitwarden_directory_connector = { description = "Main process for Bitwarden Directory Connector"; wantedBy = ["multi-user.target"]; after = ["network-online.target"]; wants = []; path = [pkgs.jq]; environment = { BITWARDENCLI_CONNECTOR_APPDATA_DIR = cfg.directory; BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS = "true"; }; serviceConfig = { Type = "oneshot"; User = "${cfg.user}"; Group = "${cfg.user}"; ExecStartPre = pkgs.writeShellScript "bitwarden_directory_connector-config" '' # create the config file ${cfg.package}/bin/${cfg.binary_name} data-file touch -- ${escapeShellArg cfg.directory}/data.json.tmp chmod 600 ${escapeShellArg cfg.directory}/data.json chmod 600 -- ${escapeShellArg cfg.directory}/data.json.tmp ${cfg.package}/bin/${cfg.binary_name} config server ${cfg.domain} # now login to set credentials export BW_CLIENTID="$(< ${escapeShellArg cfg.secrets.bitwarden.client_path_id})" export BW_CLIENTSECRET="$(< ${escapeShellArg cfg.secrets.bitwarden.client_path_secret})" ${cfg.package}/bin/${cfg.binary_name} login jq '.authenticatedAccounts[0] as $account | .[$account].directoryConfigurations.ldap |= $ldap_data | .[$account].directorySettings.organizationId |= $orgID | .[$account].directorySettings.sync |= $sync_data' \ --argjson ldap_data ${escapeShellArg ldap_data} \ --arg orgID "''${BW_CLIENTID//organization.}" \ --argjson sync_data ${escapeShellArg sync_data} \ ${escapeShellArg cfg.directory}/data.json \ > ${escapeShellArg cfg.directory}/data.json.tmp mv -f -- ${escapeShellArg cfg.directory}/data.json.tmp ${escapeShellArg cfg.directory}/data.json # final config ${cfg.package}/bin/${cfg.binary_name} config directory 0 ${cfg.package}/bin/${cfg.binary_name} config ldap.password --secretfile ${cfg.secrets.ldap} ''; ExecStart = "${cfg.package}/bin/${cfg.binary_name} sync"; ExecStartPost = pkgs.writeShellScript "bitwarden_directory_connector-cleanup" '' rm -f -- ${escapeShellArg cfg.directory}/data.json ''; }; }; }; }; meta = with lib; { maintainers = with maintainers; [Silver-Golden]; }; }