at 24.11-pre 10 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7with lib; let 8 cfg = config.services.bitwarden-directory-connector-cli; 9in { 10 options.services.bitwarden-directory-connector-cli = { 11 enable = mkEnableOption "Bitwarden Directory Connector"; 12 13 package = mkPackageOption pkgs "bitwarden-directory-connector-cli" {}; 14 15 domain = mkOption { 16 type = types.str; 17 description = "The domain the Bitwarden/Vaultwarden is accessible on."; 18 example = "https://vaultwarden.example.com"; 19 }; 20 21 user = mkOption { 22 type = types.str; 23 description = "User to run the program."; 24 default = "bwdc"; 25 }; 26 27 interval = mkOption { 28 type = types.str; 29 default = "*:0,15,30,45"; 30 description = "The interval when to run the connector. This uses systemd's OnCalendar syntax."; 31 }; 32 33 ldap = mkOption { 34 description = '' 35 Options to configure the LDAP connection. 36 If you used the desktop application to test the configuration you can find the settings by searching for `ldap` in `~/.config/Bitwarden\ Directory\ Connector/data.json`. 37 ''; 38 default = {}; 39 type = types.submodule ({ 40 config, 41 options, 42 ... 43 }: { 44 freeformType = types.attrsOf (pkgs.formats.json {}).type; 45 46 config.finalJSON = builtins.toJSON (removeAttrs config (filter (x: x == "finalJSON" || ! options.${x}.isDefined or false) (attrNames options))); 47 48 options = { 49 finalJSON = mkOption { 50 type = (pkgs.formats.json {}).type; 51 internal = true; 52 readOnly = true; 53 visible = false; 54 }; 55 56 ssl = mkOption { 57 type = types.bool; 58 default = false; 59 description = "Whether to use TLS."; 60 }; 61 startTls = mkOption { 62 type = types.bool; 63 default = false; 64 description = "Whether to use STARTTLS."; 65 }; 66 67 hostname = mkOption { 68 type = types.str; 69 description = "The host the LDAP is accessible on."; 70 example = "ldap.example.com"; 71 }; 72 73 port = mkOption { 74 type = types.port; 75 default = 389; 76 description = "Port LDAP is accessible on."; 77 }; 78 79 ad = mkOption { 80 type = types.bool; 81 default = false; 82 description = "Whether the LDAP Server is an Active Directory."; 83 }; 84 85 pagedSearch = mkOption { 86 type = types.bool; 87 default = false; 88 description = "Whether the LDAP server paginates search results."; 89 }; 90 91 rootPath = mkOption { 92 type = types.str; 93 description = "Root path for LDAP."; 94 example = "dc=example,dc=com"; 95 }; 96 97 username = mkOption { 98 type = types.str; 99 description = "The user to authenticate as."; 100 example = "cn=admin,dc=example,dc=com"; 101 }; 102 }; 103 }); 104 }; 105 106 sync = mkOption { 107 description = '' 108 Options to configure what gets synced. 109 If you used the desktop application to test the configuration you can find the settings by searching for `sync` in `~/.config/Bitwarden\ Directory\ Connector/data.json`. 110 ''; 111 default = {}; 112 type = types.submodule ({ 113 config, 114 options, 115 ... 116 }: { 117 freeformType = types.attrsOf (pkgs.formats.json {}).type; 118 119 config.finalJSON = builtins.toJSON (removeAttrs config (filter (x: x == "finalJSON" || ! options.${x}.isDefined or false) (attrNames options))); 120 121 options = { 122 finalJSON = mkOption { 123 type = (pkgs.formats.json {}).type; 124 internal = true; 125 readOnly = true; 126 visible = false; 127 }; 128 129 removeDisabled = mkOption { 130 type = types.bool; 131 default = true; 132 description = "Remove users from bitwarden groups if no longer in the ldap group."; 133 }; 134 135 overwriteExisting = mkOption { 136 type = types.bool; 137 default = false; 138 description = "Remove and re-add users/groups, See https://bitwarden.com/help/user-group-filters/#overwriting-syncs for more details."; 139 }; 140 141 largeImport = mkOption { 142 type = types.bool; 143 default = false; 144 description = "Enable if you are syncing more than 2000 users/groups."; 145 }; 146 147 memberAttribute = mkOption { 148 type = types.str; 149 description = "Attribute that lists members in a LDAP group."; 150 example = "uniqueMember"; 151 }; 152 153 creationDateAttribute = mkOption { 154 type = types.str; 155 description = "Attribute that lists a user's creation date."; 156 example = "whenCreated"; 157 }; 158 159 useEmailPrefixSuffix = mkOption { 160 type = types.bool; 161 default = false; 162 description = "If a user has no email address, combine a username prefix with a suffix value to form an email."; 163 }; 164 emailPrefixAttribute = mkOption { 165 type = types.str; 166 description = "The attribute that contains the users username."; 167 example = "accountName"; 168 }; 169 emailSuffix = mkOption { 170 type = types.str; 171 description = "Suffix for the email, normally @example.com."; 172 example = "@example.com"; 173 }; 174 175 users = mkOption { 176 type = types.bool; 177 default = false; 178 description = "Sync users."; 179 }; 180 userPath = mkOption { 181 type = types.str; 182 description = "User directory, relative to root."; 183 default = "ou=users"; 184 }; 185 userObjectClass = mkOption { 186 type = types.str; 187 description = "Class that users must have."; 188 default = "inetOrgPerson"; 189 }; 190 userEmailAttribute = mkOption { 191 type = types.str; 192 description = "Attribute for a users email."; 193 default = "mail"; 194 }; 195 userFilter = mkOption { 196 type = types.str; 197 description = "LDAP filter for users."; 198 example = "(memberOf=cn=sales,ou=groups,dc=example,dc=com)"; 199 default = ""; 200 }; 201 202 groups = mkOption { 203 type = types.bool; 204 default = false; 205 description = "Whether to sync ldap groups into BitWarden."; 206 }; 207 groupPath = mkOption { 208 type = types.str; 209 description = "Group directory, relative to root."; 210 default = "ou=groups"; 211 }; 212 groupObjectClass = mkOption { 213 type = types.str; 214 description = "A class that groups will have."; 215 default = "groupOfNames"; 216 }; 217 groupNameAttribute = mkOption { 218 type = types.str; 219 description = "Attribute for a name of group."; 220 default = "cn"; 221 }; 222 groupFilter = mkOption { 223 type = types.str; 224 description = "LDAP filter for groups."; 225 example = "(cn=sales)"; 226 default = ""; 227 }; 228 }; 229 }); 230 }; 231 232 secrets = { 233 ldap = mkOption { 234 type = types.str; 235 description = "Path to file that contains LDAP password for user in {option}`ldap.username"; 236 }; 237 238 bitwarden = { 239 client_path_id = mkOption { 240 type = types.str; 241 description = "Path to file that contains Client ID."; 242 }; 243 client_path_secret = mkOption { 244 type = types.str; 245 description = "Path to file that contains Client Secret."; 246 }; 247 }; 248 }; 249 }; 250 251 config = mkIf cfg.enable { 252 users.groups."${cfg.user}" = {}; 253 users.users."${cfg.user}" = { 254 isSystemUser = true; 255 group = cfg.user; 256 }; 257 258 systemd = { 259 timers.bitwarden-directory-connector-cli = { 260 description = "Sync timer for Bitwarden Directory Connector"; 261 wantedBy = ["timers.target"]; 262 after = ["network-online.target"]; 263 timerConfig = { 264 OnCalendar = cfg.interval; 265 Unit = "bitwarden-directory-connector-cli.service"; 266 Persistent = true; 267 }; 268 }; 269 270 services.bitwarden-directory-connector-cli = { 271 description = "Main process for Bitwarden Directory Connector"; 272 path = [pkgs.jq]; 273 274 environment = { 275 BITWARDENCLI_CONNECTOR_APPDATA_DIR = "/tmp"; 276 BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS = "true"; 277 }; 278 279 preStart = '' 280 set -eo pipefail 281 282 # create the config file 283 ${lib.getExe cfg.package} data-file 284 touch /tmp/data.json.tmp 285 chmod 600 /tmp/data.json{,.tmp} 286 287 ${lib.getExe cfg.package} config server ${cfg.domain} 288 289 # now login to set credentials 290 export BW_CLIENTID="$(< ${escapeShellArg cfg.secrets.bitwarden.client_path_id})" 291 export BW_CLIENTSECRET="$(< ${escapeShellArg cfg.secrets.bitwarden.client_path_secret})" 292 ${lib.getExe cfg.package} login 293 294 jq '.authenticatedAccounts[0] as $account 295 | .[$account].directoryConfigurations.ldap |= $ldap_data 296 | .[$account].directorySettings.organizationId |= $orgID 297 | .[$account].directorySettings.sync |= $sync_data' \ 298 --argjson ldap_data ${escapeShellArg cfg.ldap.finalJSON} \ 299 --arg orgID "''${BW_CLIENTID//organization.}" \ 300 --argjson sync_data ${escapeShellArg cfg.sync.finalJSON} \ 301 /tmp/data.json \ 302 > /tmp/data.json.tmp 303 304 mv -f /tmp/data.json.tmp /tmp/data.json 305 306 # final config 307 ${lib.getExe cfg.package} config directory 0 308 ${lib.getExe cfg.package} config ldap.password --secretfile ${cfg.secrets.ldap} 309 ''; 310 311 serviceConfig = { 312 Type = "oneshot"; 313 User = "${cfg.user}"; 314 PrivateTmp = true; 315 ExecStart = "${lib.getExe cfg.package} sync"; 316 }; 317 }; 318 }; 319 }; 320 321 meta.maintainers = with maintainers; [Silver-Golden]; 322}