feat(idp): samba ldap attributes

This commit is contained in:
arcnmx 2024-03-17 16:36:06 -07:00
parent 4ad8e34fa4
commit 81bd1a1a15
12 changed files with 670 additions and 3 deletions

View file

@ -0,0 +1,164 @@
{
config,
lib,
inputs,
pkgs,
...
}: let
inherit (inputs.self.lib.lib) mapOptionDefaults;
inherit (lib.options) mkEnableOption;
inherit (lib.modules) mkIf mkOptionDefault;
inherit (lib.attrsets) attrValues;
inherit (lib.lists) filter;
inherit (lib.strings) concatStringsSep concatMapStringsSep escapeShellArgs;
inherit (config.users) ldap;
cfg = config.users.ldap.management;
enabledObjects = filter (object: object.enable) (attrValues cfg.objects);
smbSyncUsers = filter (user: user.samba.sync.enable) (attrValues cfg.users);
smbSyncGroups = filter (group: group.samba.sync.enable) (attrValues cfg.groups);
modifyObjects = filter (object: object.changeType == "modify") enabledObjects;
addObjects = filter (object: object.changeType == "add") enabledObjects;
deleteObjects = filter (object: object.changeType == "delete") enabledObjects;
additions = pkgs.writeText "ldap-management-add.ldap" (
concatMapStringsSep "\n" (object: object.changeText) addObjects
);
# TODO: split up adds and replaces so this can be done without `ldapmodify -c`
modifications = pkgs.writeText "ldap-management-modify.ldap" (
concatMapStringsSep "\n" (object: object.changeText) modifyObjects
);
deletions = pkgs.writeText "ldap-management-delete.ldap" (
concatMapStringsSep "\n" (object: object.changeText) deleteObjects
);
objectClassAttr = "objectClass";
sidAttr = "ipaNTSecurityIdentifier";
ntHashAttr = "ipaNTHash";
authTypeAttr = "ipaUserAuthType";
userSearchAttrs = [ objectClassAttr sidAttr authTypeAttr ntHashAttr ];
groupSearchAttrs = [ objectClassAttr sidAttr ];
managementScript = pkgs.writeShellScript "ldap-management.sh" ''
set -eu
ldapsearch() {
command ldapsearch -QLLL -o ldif_wrap=no "$@"
}
ldapmodify() {
command ldapmodify -Q "$@"
}
ldap_parse() {
local LDAP_ATTR=$1 LDAP_LIMIT LDAP_LINE LDAP_COUNT=0
shift 1
local LDAP_LIMIT=''${1-1}
while read -r LDAP_LINE; do
if [[ $LDAP_LIMIT -eq 0 ]]; then
break
fi
if [[ $LDAP_LINE = "$LDAP_ATTR:: "* ]]; then
printf '%s\n' "$LDAP_LINE" | cut -d ' ' -f 2- | base64 -d
elif [[ $LDAP_LINE = "$LDAP_ATTR: "* ]]; then
printf '%s\n' "$LDAP_LINE" | cut -d ' ' -f 2-
else
continue
fi
LDAP_COUNT=$((LDAP_COUNT+1))
LDAP_LIMIT=$((LDAP_LIMIT-1))
done
if [[ $LDAP_COUNT -eq 0 ]]; then
echo "$LDAP_ATTR not found" >&2
return 1
fi
}
smbsync_group() {
local LDAP_GROUP_CN=$1 SMB_GROUP_DATA SMB_GROUP_SID
shift 1
echo "updating cn=''${LDAP_GROUP_CN},${ldap.groupDnSuffix} ..." >&2
SMB_GROUP_DATA=$(ldapsearch -z1 \
-b "${ldap.groupDnSuffix}${ldap.base}" \
"(&(cn=$LDAP_GROUP_CN)(${objectClassAttr}=posixgroup))" \
${escapeShellArgs groupSearchAttrs}
)
SMB_GROUP_SID=$(ldap_parse ${sidAttr} <<< "$SMB_GROUP_DATA")
ldapmodify <<EOF
dn: cn=$LDAP_GROUP_CN,${ldap.groupDnSuffix}${ldap.base}
changetype: modify
replace: sambaSID
sambaSID: $SMB_GROUP_SID
-
EOF
}
smbsync_user() {
local LDAP_USER_UID=$1 SMB_USER_DATA SMB_USER_SID SMB_USER_NTPASS
shift 1
echo "updating uid=''${LDAP_USER_UID},${ldap.userDnSuffix} ..." >&2
SMB_USER_DATA=$(ldapsearch -z1 \
-b "${ldap.userDnSuffix}${ldap.base}" \
"(&(uid=$LDAP_USER_UID)(${objectClassAttr}=posixaccount))" \
${escapeShellArgs userSearchAttrs}
)
SMB_USER_SID=$(ldap_parse ${sidAttr} <<< "$SMB_USER_DATA")
SMB_USER_NTPASS=$(ldap_parse ${ntHashAttr} <<< "$SMB_USER_DATA" | xxd -p)
SMB_USER_NTPASS=''${SMB_USER_NTPASS^^}
ldapmodify <<EOF
dn: uid=$LDAP_USER_UID,${ldap.userDnSuffix}${ldap.base}
changetype: modify
replace: sambaSID
sambaSID: $SMB_USER_SID
-
replace: sambaNTPassword
sambaNTPassword: $SMB_USER_NTPASS
-
EOF
}
ldapwhoami
ldapmodify -cf "$MAN_LDAP_ADD"
ldapmodify -c -f "$MAN_LDAP_MODIFY" || true
ldapmodify -f "$MAN_LDAP_DELETE"
IFS=',' declare -a 'SMB_SYNC_GROUPS=($SMB_SYNC_GROUPS)'
for SMB_GROUP_CN in "''${SMB_SYNC_GROUPS[@]}"; do
smbsync_group "$SMB_GROUP_CN"
done
IFS=',' declare -a 'SMB_SYNC_USERS=($SMB_SYNC_USERS)'
for SMB_USER_UID in "''${SMB_SYNC_USERS[@]}"; do
smbsync_user "$SMB_USER_UID"
done
'';
in {
options.users.ldap.management = with lib.types; {
enable = mkEnableOption "LDAP object management";
};
config = mkIf cfg.enable {
systemd.services.ldap-management = rec {
wantedBy = [ "multi-user.target" ];
wants = [ "krb5-host.service" ];
after = wants;
path = [ config.services.openldap.package pkgs.coreutils pkgs.xxd ];
environment = mapOptionDefaults {
LDAPBASE = ldap.base;
LDAPURI = "ldaps://ldap.int.${config.networking.domain}";
LDAPSASL_MECH = "GSSAPI";
LDAPSASL_AUTHCID = "dn:krbprincipalname=host/${config.networking.fqdn}@${config.security.ipa.realm},cn=services,cn=accounts,${ldap.base}";
# LDAPBINDDN?
SMB_SYNC_GROUPS = concatStringsSep "," (map (group: group.name) smbSyncGroups);
SMB_SYNC_USERS = concatStringsSep "," (map (user: user.uid) smbSyncUsers);
MAN_LDAP_ADD = "${additions}";
MAN_LDAP_MODIFY = "${modifications}";
MAN_LDAP_DELETE = "${deletions}";
};
serviceConfig = {
Type = mkOptionDefault "oneshot";
ExecStart = [ "${managementScript}" ];
};
};
};
}

View file

@ -0,0 +1,175 @@
{
config,
lib,
inputs,
...
}: let
inherit (inputs.self.lib.lib) unmerged mkAlmostOptionDefault;
inherit (lib.options) mkOption mkEnableOption;
inherit (lib.modules) mkIf mkOptionDefault;
inherit (lib.attrsets) filterAttrs mapAttrsToList nameValuePair;
inherit (lib.lists) optional toList;
inherit (lib.strings) toLower removeSuffix concatMapStrings concatStringsSep optionalString;
ldap'lib = {
specialArgs = {
nixosConfig = config;
ldap = config.users.ldap // {
lib = config.lib.ldap;
};
};
objectModule = ldapObjectModule;
objectType = lib.types.submoduleWith {
modules = [ ldapObjectModule ];
inherit (ldap'lib) specialArgs;
};
objectSettingType' = lib.types.submoduleWith {
modules = [ ldapObjectSettingModule ];
inherit (ldap'lib) specialArgs;
};
objectSettingType = let
mapToObjectSetting = value: {
inherit value;
};
in lib.types.coercedTo ldapValueType mapToObjectSetting ldap'lib.objectSettingType';
objectSettingsModule = ldapObjectSettingsModule;
objectSettingsType = lib.types.submoduleWith {
modules = [ ldapObjectSettingsModule ];
inherit (ldap'lib) specialArgs;
};
mapObjectSettingsToPair = settings: nameValuePair
(removeSuffix ",${config.users.ldap.base}" settings.dn)
(unmerged.mergeAttrs settings.settings);
mapObjectSettingsToAttr = settings: let
pair = ldap'lib.mapObjectSettingsToPair settings;
in {
${pair.name} = pair.value;
};
mkLdapModifyObjectSettingValues = let
mkLdapModifyValues = setting: concatMapStrings (value: ''
${setting.key}: ${toString value}
'') (toList setting.value);
in mkLdapModifyValues;
mkLdapModifyObjectSettings = let
mkLdapModifySetting = setting: ''
${setting.modifyType}: ${setting.key}
'' + ldap'lib.mkLdapModifyObjectSettingValues setting;
in settings: mapAttrsToList (_: mkLdapModifySetting) settings;
mkLdapAddObjectSettings = settings: mapAttrsToList (_: ldap'lib.mkLdapModifyObjectSettingValues) settings;
mkLdapModifyObject = let
mkHeader = changeType: object: ''
dn: ${object.dn}
changetype: ${changeType}
'';
in {
modify = object: let
enabledSettings' = filterAttrs (_: setting: setting.enable) object.settings;
enabledSettings = ldap'lib.mkLdapModifyObjectSettings enabledSettings';
replaceSettings' = filterAttrs (_: setting: setting.modifyType == "replace") enabledSettings';
replaceSettings = ldap'lib.mkLdapModifyObjectSettings replaceSettings';
addSettings' = filterAttrs (_: setting: setting.modifyType == "add") enabledSettings';
replaceText = mkHeader "modify" object + concatStringsSep "-\n" replaceSettings;
text = mkHeader "modify" object + concatStringsSep "-\n" enabledSettings;
in concatStringsSep "-\n\n" (
[ text ]
++ optional (addSettings' != { }) replaceText
);
add = object: let
enabledSettings = filterAttrs (_: setting: setting.enable) object.settings;
addSettings = ldap'lib.mkLdapAddObjectSettings enabledSettings;
in mkHeader "add" object + concatStringsSep "-\n" addSettings;
delete = object: mkHeader "delete" object;
modrdn = object: { newrdn, deleteoldrdn, newsuperior }: let
modifySettings = ''
newrdn: ${newrdn}
deleteoldrdn: ${if deleteoldrdn == true then "1" else if deleteoldrdn == "false" then "0" else toString deleteoldrdn}
'' + optionalString (newsuperior != null) ''
newsuperior: ${newsuperior}
'';
in mkHeader "modrdn" + modifySettings;
moddn = object: { deleteoldrdn, newsuperior }: let
modifySettings = ''
deleteoldrdn: ${if deleteoldrdn == true then "1" else if deleteoldrdn == "false" then "0" else toString deleteoldrdn}
newsuperior: ${newsuperior}
'';
in mkHeader "moddn" + modifySettings;
};
};
ldapPrimitiveType = with lib.types; oneOf [ str int ];
ldapValueType = with lib.types; oneOf [ ldapPrimitiveType (listOf ldapPrimitiveType) ];
ldapObjectSettingModule = {config, name, ...}: {
options = with lib.types; {
enable = mkEnableOption "setting" // {
default = true;
};
key = mkOption {
type = str;
default = name;
};
value = mkOption {
type = ldapValueType;
};
modifyType = mkOption {
type = enum [ "replace" "add" "delete" ];
default = if toLower config.key == "objectclass" then "add" else "replace";
};
};
};
ldapObjectSettingsModule = {config, ...}: {
options = with lib.types; {
enable = mkEnableOption "object" // {
default = true;
};
dn = mkOption {
type = str;
};
settings = mkOption {
type = unmerged.types.attrs;
};
};
config = {
settings = {
dn = mkAlmostOptionDefault config.dn;
};
};
};
ldapObjectModule = {config, name, ldap, ...}: {
options = with lib.types; {
enable = mkEnableOption "object creation" // {
default = true;
};
dn = mkOption {
type = str;
default = "${name},${ldap.base}";
};
changeType = mkOption {
type = enum [ "modify" "add" "delete" "modrdn" "moddn" ];
default = "modify";
};
changeText = mkOption {
type = lines;
};
objectClasses = mkOption {
type = listOf str;
default = [ ];
};
settings = mkOption {
type = attrsOf ldap.lib.objectSettingType;
default = { };
};
};
config = {
settings = {
objectClass = mkIf (config.objectClasses != [ ]) (mkOptionDefault config.objectClasses);
};
changeText = mkOptionDefault (ldap'lib.mkLdapModifyObject.${config.changeType} config);
};
};
in {
options.users.ldap = with lib.types; {
management.objects = mkOption {
type = attrsOf ldap'lib.objectType;
default = { };
};
};
config.lib.ldap = ldap'lib;
}

View file

@ -0,0 +1,201 @@
{
config,
lib,
inputs,
...
}: let
inherit (inputs.self.lib.lib) mkAlmostOptionDefault mapListToAttrs;
inherit (lib.options) mkOption mkEnableOption;
inherit (lib.modules) mkIf mkDefault mkOptionDefault;
inherit (lib.attrsets) mapAttrs mapAttrsToList;
inherit (lib.lists) filter;
inherit (lib.strings) concatStrings;
cfg = config.users.ldap;
ldap'lib = config.lib.ldap;
userModule = {config, nixosConfig, name, ldap, ...}: let
user = nixosConfig.users.users.${config.user.name};
sambaAccountFlags = {
normalUser = "U";
disabled = "D";
homeRequired = "H";
temporaryDuplicate = "T";
mnsLogon = "M";
workstationTrust = "W";
serverTrust = "S";
domainTrust = "I";
autoLock = "L";
noPasswordExpiry = "X";
noPasswordRequired = "N";
};
mkSambaAccountFlags = flags: let
empty = " ";
flagChar = char: flag: if flag then char else empty;
flagChars = mapAttrsToList (name: flagChar sambaAccountFlags.${name}) flags;
in "[${concatStrings flagChars}]";
in {
options = with lib.types; {
user = {
enable = mkEnableOption "system user";
name = mkOption {
type = nullOr str;
default = null;
};
uid = mkOption {
type = nullOr int;
default = null;
};
};
uid = mkOption {
type = str;
default = name;
};
samba = {
enable = mkEnableOption "SMB user";
sync.enable = mkEnableOption "IPA data sync";
sid = mkOption {
type = nullOr str;
default = null;
};
accountFlags = mapAttrs (_: _: mkOption {
type = bool;
default = false;
}) sambaAccountFlags;
};
object = mkOption {
type = ldap.lib.objectSettingsType;
};
};
config = {
user = {
name = mkIf config.user.enable (mkAlmostOptionDefault name);
uid = mkIf (config.user.name != null) (mkAlmostOptionDefault user.uid);
};
samba = {
sid = mkIf (ldap.samba.domainSID != null && config.user.uid != null) (
mkAlmostOptionDefault "${ldap.samba.domainSID}-${toString (ldap.samba.sidUserOffset + config.user.uid)}"
);
accountFlags = {
normalUser = mkIf (config.user.name != null) (mkAlmostOptionDefault user.isNormalUser);
};
};
object = {
enable = mkAlmostOptionDefault config.samba.enable;
dn = mkOptionDefault "uid=${config.uid},${ldap.userDnSuffix}${ldap.base}";
settings = {
objectClasses = mkIf config.samba.enable [ "sambaSamAccount" ];
settings = mkIf config.samba.enable {
sambaSID = mkIf (config.samba.sid != null) (mkOptionDefault config.samba.sid);
sambaAcctFlags = mkOptionDefault (mkSambaAccountFlags config.samba.accountFlags);
};
};
};
};
};
groupModule = {config, nixosConfig, name, ldap, ...}: let
group = nixosConfig.users.groups.${config.group.name};
in {
options = with lib.types; {
group = {
enable = mkEnableOption "system group";
name = mkOption {
type = nullOr str;
default = null;
};
gid = mkOption {
type = nullOr int;
default = null;
};
};
name = mkOption {
type = str;
default = name;
};
samba = {
enable = mkEnableOption "SMB group";
sync.enable = mkEnableOption "IPA data sync";
sid = mkOption {
type = nullOr str;
default = null;
};
groupType = mkOption {
type = int;
default = 2;
description = "http://pig.made-it.com/samba-accounts.html#22762";
};
};
object = mkOption {
type = ldap.lib.objectSettingsType;
};
};
config = {
group = {
name = mkIf config.group.enable (mkAlmostOptionDefault name);
gid = mkIf (config.group.name != null) (mkAlmostOptionDefault group.gid);
};
samba = {
sid = mkIf (ldap.samba.domainSID != null && config.group.gid != null) (
mkAlmostOptionDefault "${ldap.samba.domainSID}-${toString (ldap.samba.sidGroupOffset + config.group.gid)}"
);
};
object = {
enable = mkAlmostOptionDefault config.samba.enable;
dn = mkOptionDefault "cn=${config.name},${ldap.groupDnSuffix}${ldap.base}";
settings = {
objectClasses = mkIf config.samba.enable [ "sambaGroupMapping" ];
settings = mkIf config.samba.enable {
sambaSID = mkIf (config.samba.sid != null) (mkOptionDefault config.samba.sid);
sambaGroupType = mkOptionDefault config.samba.groupType;
};
};
};
};
};
in {
options.users.ldap = with lib.types; {
management = {
users = mkOption {
type = attrsOf (submoduleWith {
modules = [ userModule ];
inherit (config.lib.ldap) specialArgs;
});
default = { };
};
groups = mkOption {
type = attrsOf (submoduleWith {
modules = [ groupModule ];
inherit (config.lib.ldap) specialArgs;
});
default = { };
};
};
samba = {
domainSID = mkOption {
type = nullOr str;
default = null;
};
sidUserOffset = mkOption {
type = int;
default = -7999;
};
sidGroupOffset = mkOption {
type = int;
default = 256 + 1;
};
};
userDnSuffix = mkOption {
type = str;
default = "";
};
groupDnSuffix = mkOption {
type = str;
default = "";
};
};
config.users.ldap = {
management.objects = let
userObjects = mapAttrsToList (_: user: user.object) cfg.management.users;
groupObjects = mapAttrsToList (_: group: group.object) cfg.management.groups;
enabledObjects = filter (object: object.enable) (userObjects ++ groupObjects);
in mapListToAttrs ldap'lib.mapObjectSettingsToPair enabledObjects;
};
}