285 lines
10 KiB
Nix
285 lines
10 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;
|
|
membermail = mkProvisionEmail name;
|
|
mail = "env:EMAIL_${lib.toUpper name}";
|
|
groups = [ lconfig.groups.member ];
|
|
membermaildiskquota = 100*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
|
|
});
|
|
|
|
mkProvisionUserAdmin = name: config.mine.shared.lib.ldap.mkScope (lconfig: llib: {
|
|
user_id = name;
|
|
membermail = mkProvisionEmail name;
|
|
groups = [ lconfig.groups.admin lconfig.groups.member ];
|
|
membermaildiskquota = 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;
|
|
};
|
|
};
|
|
}
|