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