server-configs/machines/gerd/services/lldap/default.nix
2025-03-14 16:45:16 +01:00

297 lines
11 KiB
Nix

{ 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<ServerBuilder> {
))?;
}
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<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
#[instrument(skip_all, level = "debug")]
async fn do_extended_request(&mut self, request: &LdapExtendedRequest) -> Vec<LdapOp> {
- 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";
};
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;
groups = with lconfig.groups; [ admin nextcloud_admin grafana_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;
};
};
}