{ config, lib, pkgs, ... }: let svc_domain = "ldap.${config.mine.shared.settings.domain}"; resetPasswordStartPatch = pkgs.writeText "lldap-reset-password-start.patch" '' diff --git a/server/src/main.rs b/server/src/main.rs index 6f42473..b3746a1 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -171,7 +171,7 @@ async fn set_up_server(config: Configuration) -> Result { ))?; } if config.force_update_private_key || config.force_ldap_user_pass_reset.is_yes() { - bail!("Restart the server without --force-update-private-key or --force-ldap-user-pass-reset to continue."); + // bail!("Restart the server without --force-update-private-key or --force-ldap-user-pass-reset to continue."); } let server_builder = infra::ldap_server::build_ldap_server( &config, ''; whoamiPatch = pkgs.writeText "lldap-whoami.patch" '' diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs index 7257c31..feda03c 100644 --- a/server/src/infra/ldap_handler.rs +++ b/server/src/infra/ldap_handler.rs @@ -26,7 +26,7 @@ use ldap3_proto::proto::{ LdapDerefAliases, LdapExtendedRequest, LdapExtendedResponse, LdapFilter, LdapModify, LdapModifyRequest, LdapModifyType, LdapOp, LdapPartialAttribute, LdapPasswordModifyRequest, LdapResult as LdapResultOp, LdapResultCode, LdapSearchRequest, LdapSearchResultEntry, - LdapSearchScope, + LdapSearchScope, OID_PASSWORD_MODIFY, OID_WHOAMI, }; use std::collections::HashMap; use tracing::{debug, instrument, warn}; @@ -181,7 +181,7 @@ fn root_dse_response(base_dn: &str) -> LdapOp { LdapPartialAttribute { atype: "supportedExtension".to_string(), // Password modification extension. - vals: vec![b"1.3.6.1.4.1.4203.1.11.1".to_vec()], + vals: vec![OID_PASSWORD_MODIFY.as_bytes().to_vec()], }, LdapPartialAttribute { atype: "supportedControl".to_string(), @@ -204,6 +204,11 @@ fn root_dse_response(base_dn: &str) -> LdapOp { atype: "isGlobalCatalogReady".to_string(), vals: vec![b"false".to_vec()], }, + LdapPartialAttribute { + atype: "supportedExtension".to_string(), + // whoami extension. + vals: vec![OID_WHOAMI.as_bytes().to_vec()], + }, ], }) } @@ -413,16 +418,33 @@ impl LdapHandler Vec { - match LdapPasswordModifyRequest::try_from(request) { - Ok(password_request) => self + if let Ok(password_request) = LdapPasswordModifyRequest::try_from(request) { + return self .do_password_modification(&password_request) .await - .unwrap_or_else(|e: LdapError| vec![make_extended_response(e.code, e.message)]), - Err(_) => vec![make_extended_response( - LdapResultCode::UnwillingToPerform, - format!("Unsupported extended operation: {}", &request.name), - )], + .unwrap_or_else(|e: LdapError| vec![make_extended_response(e.code, e.message)]); } + + if request.name == OID_WHOAMI { + let dn = self + .user_info + .as_ref() + .map(|user_info| { + format!( + "dn:uid={},ou=people,{}", + user_info.user.clone().into_string(), + self.ldap_info.base_dn_str + ) + }) + .unwrap_or_default(); + + return vec![make_extended_response(LdapResultCode::Success, dn)]; + } + + return vec![make_extended_response( + LdapResultCode::UnwillingToPerform, + format!("Unsupported extended operation: {}", &request.name), + )]; } async fn handle_modify_change( ''; pkgLLDAPCli = pkgs.callPackage ./../../../../shared/pkgs/lldap-cli.nix {}; in { imports = [ ./provision.nix ]; environment.systemPackages = [ pkgLLDAPCli ]; services.lldap = { enable = true; package = pkgs.lldap.overrideAttrs (old: { patches = old.patches ++ [ resetPasswordStartPatch whoamiPatch ]; }); settings = { verbose = true; ldap_user_email = "fricloudlldap.grief462@simplelogin.com"; ldap_base_dn = config.mine.shared.settings.ldap.dc; }; environment = { # always set admin password on startup LLDAP_LDAP_USER_PASS_FILE = config.age.secrets.lldap-admin-user-pass.path; LLDAP_FORCE_LDAP_USER_PASS_RESET = "true"; }; }; services.nginx.virtualHosts."${svc_domain}" = { forceSSL = true; enableACME = true; locations."/".proxyPass = "http://localhost:${builtins.toString config.services.lldap.settings.http_port}"; }; # persistent files environment.persistence.root.directories = [ { directory = "/var/lib/private/lldap"; mode = "0700"; } ]; # lldap user + setup secrets owner (need to add user for secrets to work) users.users.lldap = { group = "lldap"; isSystemUser = true; }; users.groups.lldap = {}; age.secrets = { lldap-admin-user-pass.owner = "lldap"; }; # set settings other services can use # CN = Common Name # OU = Organizational Unit # DC = Domain Component # # The users are all located in ou=people, + the base DN, so by default user bob is at cn=bob,ou=people,dc=example,dc=com. # Similarly, the groups are located in ou=groups, so the group family will be at cn=family,ou=groups,dc=example,dc=com. # Testing group membership through memberOf is supported, so you can have a filter like: (memberOf=cn=admins,ou=groups,dc=example,dc=com). mine.shared.settings.ldap = rec { host = "localhost"; port = 3890; url = "ldap://${host}:${builtins.toString port}"; dc = "dc=${config.mine.shared.settings.domain_sld},dc=${config.mine.shared.settings.domain_tld}"; bind_dn = "uid=${users.bind},ou=${ou.users},${dc}"; search_base = "ou=${ou.users},${dc}"; user_filter = ph: let attrs = [ attr.uid attr.email ]; in config.mine.shared.lib.ldap.mkFilter (lconfig: llib: llib.mkAnd [ (llib.mkGroup lconfig.groups.member) (llib.mkOr (lib.forEach attrs (v: llib.mkSearch v ph))) ] ); oc = { person = "person"; mailAccount = "mailAccount"; groupOfUniqueNames = "groupOfUniqueNames"; }; provision = config.services.lldap.provision; users = { admin = "admin"; # bind = "bind_user"; } // (lib.mapAttrs (n: v: v.user_id) config.services.lldap.provision.users); groups = { admin = "lldap_admin"; member = "base_member"; password_manager = "lldap_password_manager"; strict_readonly = "lldap_strict_readonly"; # system = "system_service"; # system_email = "system_email"; } // (lib.mapAttrs (n: v: v.display_name) config.services.lldap.provision.groups); ou = { groups = "groups"; users = "people"; }; attr = let toCamelCase = w: let parts = lib.splitString "_" w; cap = lib.imap0 (i: v: if i == 0 then v else (lib.toUpper (lib.substring 0 1 v)) + (lib.substring 1 (lib.stringLength v) v)); in lib.concatStrings (cap parts); in { uid = "uid"; firstname = "givenName"; lastname = "sn"; email = "mail"; avatar = "jpegPhoto"; groupname = "cn"; # custom # member_email = "member_email"; # mail_disk_quota = "mail_disk_quota"; } // (lib.mapAttrs (n: v: toCamelCase v.name) config.services.lldap.provision.user_attributes); age_secret = config.age.secrets.lldap-bind-user-pass.path; }; mine.shared.lib.ldap = rec { mkGroup = group_name: "memberof=cn=${group_name},ou=${config.mine.shared.settings.ldap.ou.groups},${config.mine.shared.settings.ldap.dc}"; mkOC = object_class_name: "objectclass=${object_class_name}"; mkSearch = attribute: ph: "${attribute}=${ph}"; mkFilterAdvanced = expr: let isExpr = value: if value ? type then true else false; __mkExpr = value: if isExpr value then mkFilterAdvanced value else "(${value})"; _mkExpr = op: value: "(${op}" + (builtins.concatStringsSep "" (lib.forEach value (v: __mkExpr v))) + ")"; mkExpr = expr: assert isExpr expr; if expr.type == "and" then _mkExpr "&" expr.values else _mkExpr "|" expr.values; in mkExpr expr; mkAndOr = andExprs: orExprs: mkFilterAdvanced { type = "and"; values = andExprs ++ [ { type = "or"; values = orExprs; } ]; }; mkFilter = t: mkFilterAdvanced (t config.mine.shared.settings.ldap config.mine.shared.lib.ldap); mkScope = t: t config.mine.shared.settings.ldap config.mine.shared.lib.ldap; mkAnd = v: { type = "and"; values = v; }; mkOr = v: { type = "or"; values = v; }; # mkProvision helpers for creating users mkProvisionEmail = name: "${name}@${config.mine.shared.settings.domain}"; mkProvisionUserNormal = name: config.mine.shared.lib.ldap.mkScope (lconfig: llib: { user_id = name; display_name = name; # required for nextcloud membermail = mkProvisionEmail name; mail = "env:EMAIL_${lib.toUpper name}"; groups = [ lconfig.groups.member ]; membermaildiskquota = 100*1024*1024; # mb nextcloudquota = 5*1024*1024; # mb }); mkProvisionUserSystem = name: password_file: config.mine.shared.lib.ldap.mkScope (lconfig: llib: { user_id = name; membermail = mkProvisionEmail name; mail = mkProvisionEmail name; password = "file:${password_file}"; groups = [ lconfig.groups.system_mail lconfig.groups.system_service ]; membermaildiskquota = 10*1024*1024; # mb }); mkProvisionUserSystemExt = name: password_file: custom_attrs: lib.recursiveUpdate (config.mine.shared.lib.ldap.mkScope (lconfig: llib: { user_id = name; membermail = mkProvisionEmail name; password = "file:${password_file}"; groups = [ lconfig.groups.system_mail lconfig.groups.system_service ]; membermaildiskquota = 10*1024*1024; # mb })) custom_attrs; mkProvisionUserAdmin = name: config.mine.shared.lib.ldap.mkScope (lconfig: llib: { user_id = name; display_name = name; # required for nextcloud membermail = mkProvisionEmail name; mail = mkProvisionEmail name; groups = with lconfig.groups; [ admin nextcloud_admin grafana_admin drasl_admin member ]; membermaildiskquota = 100*1024*1024; # mb nextcloudquota = 100*1024*1024; # mb }); }; mine.shared.meta.lldap = { name = "LDAP"; description = "We host our own LDAP server, you can use it to change your displayname, name, password, etc."; url = "https://${svc_domain}"; package = let pkg = config.services.lldap.package; in { name = pkg.pname; version = pkg.version; meta = pkg.meta; }; }; }