{ pkgs, config, lib, ... }: with lib; let cfg = config.services.bitwarden_directory_connector; ldap_data = builtins.toJSON { ssl = cfg.ldap.ssl; startTls = cfg.ldap.startTls; sslAllowUnauthorized = cfg.ldap.sslAllowUnauthorized; port = cfg.ldap.port; currentUser = false; ad = cfg.ldap.ad; pagedSearch = true; password = "to_be_replaced"; hostname = cfg.ldap.hostname; rootPath = cfg.ldap.root; username = cfg.ldap.username; }; sync_data = builtins.toJSON ({ removeDisabled = cfg.sync.removeDisabled; overwriteExisting = cfg.sync.overwriteExisting; largeImport = cfg.sync.largeImport; creationDateAttribute = cfg.sync.creationDateAttribute; memberAttribute = cfg.sync.memberAttribute; interval = 5; useEmailPrefixSuffix = cfg.sync.emailPrefixSuffix.enable; users = cfg.sync.users.enable; groups = cfg.sync.groups.enable; } // optionalAttrs cfg.sync.emailPrefixSuffix.enable { emailPrefixAttribute = cfg.sync.emailPrefixSuffix.prefixAttribute; emailSuffix = cfg.sync.emailPrefixSuffix.suffix; } // optionalAttrs cfg.sync.users.enable { userPath = cfg.sync.users.path; userObjectClass = cfg.sync.users.objectClass; userEmailAttribute = cfg.sync.users.emailAttribute; userFilter = cfg.sync.users.filter; } // optionalAttrs cfg.sync.groups.enable { groupPath = cfg.sync.groups.path; groupObjectClass = cfg.sync.groups.objectClass; groupNameAttribute = cfg.sync.groups.nameAttribute; groupFilter = cfg.sync.groups.filter; }); json_string = string: builtins.replaceStrings ["\""] ["\\\""] string; 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"; }; ldap = { 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 ""; }; ad = mkOption { type = types.bool; default = false; description = lib.mdDoc "Is Active Directory."; }; port = mkOption { type = types.int; default = 389; description = lib.mdDoc "Port LDAP is accessable on"; }; hostname = mkOption { type = types.str; description = lib.mdDoc "The host the LDAP is accessable on."; example = "ldap.example.com"; }; root = 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"; }; pw_env = mkOption { type = types.str; description = lib.mdDoc "The ENV var that the ldap password is stored."; default = "LDAP_PW"; }; }; sync = { interval = mkOption { type = types.str; default = "*:0,15,30,45"; description = lib.mdDoc "When to run the connector, OnCalendar syntax."; }; 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"; }; emailPrefixSuffix = { enable = 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."; }; prefixAttribute = mkOption { type = types.str; description = lib.mdDoc "Attribute that has a users username."; example = "accountName"; }; suffix = mkOption { type = types.str; description = lib.mdDoc "Suffix for the email, normally @example.com."; example = "@example.com"; }; }; users = { enable = mkOption { type = types.bool; default = false; description = lib.mdDoc "Sync users."; }; path = mkOption { type = types.str; description = lib.mdDoc "User directory, relative to root."; example = "ou=users"; }; objectClass = mkOption { type = types.str; description = lib.mdDoc "A class that users will have."; example = "inetOrgPerson"; }; emailAttribute = mkOption { type = types.str; description = lib.mdDoc "Attribute for a users email."; example = "mail"; }; filter = mkOption { type = types.str; description = lib.mdDoc "Filter for users."; example = "(memberOf=cn=sales,ou=groups,dc=example,dc=com)"; }; }; groups = { enable = mkOption { type = types.bool; default = false; description = lib.mdDoc "Sync groups."; }; path = mkOption { type = types.str; description = lib.mdDoc "Group directory, relative to root."; example = "ou=groups"; }; objectClass = mkOption { type = types.str; description = lib.mdDoc "A class that groups will have."; example = "groupOfNames"; }; nameAttribute = mkOption { type = types.str; description = lib.mdDoc "Attribute for a name of group."; example = "cn"; }; filter = mkOption { type = types.str; description = lib.mdDoc "Filter for groups."; example = "(cn=sales)"; }; }; }; env = { ldap = mkOption rec { type = types.str; description = "Auth for the LDAP, has value defined in {option}`ldap.pw_env"; }; bitwarden = mkOption rec { type = types.str; description = "Auth for Bitwarden, has BW_CLIENTID and BW_CLIENTSECRET"; }; }; }; 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.sync.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 ${cfg.package}/bin/${cfg.binary_name} config server ${cfg.domain} # now login to set credentials ${cfg.package}/bin/${cfg.binary_name} login # set the ldap details account=$(jq '.authenticatedAccounts[0]?' ${cfg.directory}/data.json) jq ".[$account].directoryConfigurations.ldap |= ${json_string ldap_data}" ${cfg.directory}/data.json > ${cfg.directory}/data1.json # remove the original rm -f ${cfg.directory}/data.json # set the client id orgID=$(echo $BW_CLIENTID | sed 's/organization\.//g') jq ".[$account].directorySettings.organizationId |= \"$orgID\" " ${cfg.directory}/data1.json > ${cfg.directory}/data2.json # and sync data jq ".[$account].directorySettings.sync |= ${json_string sync_data}" ${cfg.directory}/data2.json > ${cfg.directory}/data.json # final config ${cfg.package}/bin/${cfg.binary_name} config directory 0 ${cfg.package}/bin/${cfg.binary_name} config ldap.password --secretenv ${cfg.ldap.pw_env} # cleanup temp files rm -f ${cfg.directory}/data1.json rm -f ${cfg.directory}/data2.json ''; ExecStart = ''${cfg.package}/bin/${cfg.binary_name} sync''; EnvironmentFile = [ "${cfg.env.ldap}" "${cfg.env.bitwarden}" ]; }; }; }; }; meta = with lib; { maintainers = with maintainers; [Silver-Golden]; }; }