Skip to content

Commit e4693f3

Browse files
authored
Merge pull request #8 from coreweave/homedirectory-override
feat: Add override_home_directory for scim and ldap
2 parents 83b40e6 + d55ae4a commit e4693f3

File tree

7 files changed

+171
-1
lines changed

7 files changed

+171
-1
lines changed

examples/nsscache-scim.conf

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ scim_path_login_shell = urn:example:params:scim:schemas:extension:User/loginShel
6262
# (Optional) Default shell if not specified for the user
6363
scim_default_shell = /bin/bash
6464

65+
# (Optional) Override home directory for all users
66+
# If set, this takes precedence over scim_path_home_directory
67+
# Use %%u to substitute the username (optional)
68+
# Examples:
69+
# scim_override_home_directory = /mnt/home/%%u # Per-user: /mnt/home/john
70+
# scim_override_home_directory = /shared/home # Same for all: /shared/home
71+
scim_override_home_directory = /mnt/home/%%u
72+
6573
[sshkey]
6674

6775
scim_path_username = urn:example:params:scim:schemas:extension:User/userName

nss_cache/sources/ldapsource.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -865,7 +865,9 @@ def Transform(self, obj):
865865
pw.uid = int(pw.uid + self.conf["offset"])
866866
pw.gid = int(pw.gid + self.conf["offset"])
867867

868-
if self.conf.get("home_dir"):
868+
if "override_home_dir" in self.conf:
869+
pw.dir = self.conf.get("override_home_dir", "").replace("%u", pw.name)
870+
elif self.conf.get("home_dir"):
869871
pw.dir = "/home/%s" % pw.name
870872
elif "unixHomeDirectory" in obj:
871873
pw.dir = obj["unixHomeDirectory"][0]

nss_cache/sources/ldapsource_test.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1267,6 +1267,100 @@ def testVerifyRID(self):
12671267
serverctrls=self.compareSPRC(),
12681268
)
12691269

1270+
def testGetPasswdMapWithHomeDirectoryOverride(self):
1271+
test_posix_account = (
1272+
"cn=test,ou=People,dc=example,dc=com",
1273+
{
1274+
"sambaSID": ["S-1-5-21-2127521184-1604012920-1887927527-72713"],
1275+
"uidNumber": [1000],
1276+
"gidNumber": [1000],
1277+
"uid": ["test"],
1278+
"cn": ["Testguy McTest"],
1279+
"homeDirectory": ["/home/test"],
1280+
"loginShell": ["/bin/sh"],
1281+
"userPassword": ["p4ssw0rd"],
1282+
"modifyTimestamp": ["20070227012807Z"],
1283+
},
1284+
)
1285+
config = dict(self.config)
1286+
config["override_home_dir"] = "/mnt/home/%u"
1287+
attrlist = [
1288+
"uid",
1289+
"uidNumber",
1290+
"gidNumber",
1291+
"gecos",
1292+
"cn",
1293+
"homeDirectory",
1294+
"loginShell",
1295+
"fullName",
1296+
"modifyTimestamp",
1297+
]
1298+
self.ldap_mock.ReconnectLDAPObject.return_value.result3.side_effect = [
1299+
(ldap.RES_SEARCH_ENTRY, [test_posix_account], None, []),
1300+
(ldap.RES_SEARCH_RESULT, None, None, []),
1301+
]
1302+
1303+
source = ldapsource.LdapSource(config)
1304+
data = source.GetPasswdMap()
1305+
1306+
self.assertEqual(1, len(data))
1307+
first = data.PopItem()
1308+
self.assertEqual("/mnt/home/test", first.dir)
1309+
self.ldap_mock.ReconnectLDAPObject.return_value.search_ext.assert_called_with(
1310+
base=mock.ANY,
1311+
filterstr=mock.ANY,
1312+
scope=mock.ANY,
1313+
attrlist=attrlist,
1314+
serverctrls=self.compareSPRC(),
1315+
)
1316+
1317+
def testGetPasswdMapWithHomeDirectoryOverrideNoSubstitution(self):
1318+
test_posix_account = (
1319+
"cn=test,ou=People,dc=example,dc=com",
1320+
{
1321+
"sambaSID": ["S-1-5-21-2127521184-1604012920-1887927527-72713"],
1322+
"uidNumber": [1000],
1323+
"gidNumber": [1000],
1324+
"uid": ["test"],
1325+
"cn": ["Testguy McTest"],
1326+
"homeDirectory": ["/home/test"],
1327+
"loginShell": ["/bin/sh"],
1328+
"userPassword": ["p4ssw0rd"],
1329+
"modifyTimestamp": ["20070227012807Z"],
1330+
},
1331+
)
1332+
config = dict(self.config)
1333+
config["override_home_dir"] = "/shared/home"
1334+
attrlist = [
1335+
"uid",
1336+
"uidNumber",
1337+
"gidNumber",
1338+
"gecos",
1339+
"cn",
1340+
"homeDirectory",
1341+
"loginShell",
1342+
"fullName",
1343+
"modifyTimestamp",
1344+
]
1345+
self.ldap_mock.ReconnectLDAPObject.return_value.result3.side_effect = [
1346+
(ldap.RES_SEARCH_ENTRY, [test_posix_account], None, []),
1347+
(ldap.RES_SEARCH_RESULT, None, None, []),
1348+
]
1349+
1350+
source = ldapsource.LdapSource(config)
1351+
data = source.GetPasswdMap()
1352+
1353+
self.assertEqual(1, len(data))
1354+
first = data.PopItem()
1355+
self.assertEqual("/shared/home", first.dir)
1356+
self.ldap_mock.ReconnectLDAPObject.return_value.search_ext.assert_called_with(
1357+
base=mock.ANY,
1358+
filterstr=mock.ANY,
1359+
scope=mock.ANY,
1360+
attrlist=attrlist,
1361+
serverctrls=self.compareSPRC(),
1362+
)
1363+
12701364

12711365
class TestUpdateGetter(unittest.TestCase):
12721366
def setUp(self):

nss_cache/sources/scimsource.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,12 @@ def _ExtractGecos(self, user_data):
511511

512512
def _ExtractHomeDir(self, user_data):
513513
"""Extract home directory using configurable path."""
514+
# Check for override_home_directory first
515+
override_home = self._GetMapConfig("override_home_directory", "")
516+
if override_home:
517+
username = self._ExtractUsername(user_data)
518+
return override_home.replace("%u", username) if username else override_home
519+
514520
home_path = self._GetMapConfig("scim_path_home_directory", "")
515521

516522
# Try the configured path first

nss_cache/sources/scimsource_test.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,44 @@ def testReadEntryMissingUid(self):
578578

579579
self.assertIsNone(entry)
580580

581+
def testReadEntryWithHomeDirectoryOverride(self):
582+
"""Test _ReadEntry with home directory override."""
583+
self.mock_source.conf.update({
584+
"override_home_directory": "/mnt/home/%u"
585+
})
586+
user_data = {
587+
"id": "1001",
588+
"userName": "testuser",
589+
"homeDirectory": "/home/testuser",
590+
"loginShell": "/bin/bash",
591+
"name": {"formatted": "Test User"}
592+
}
593+
594+
entry = self.parser._ReadEntry(user_data)
595+
596+
self.assertIsNotNone(entry)
597+
self.assertEqual(entry.name, "testuser")
598+
self.assertEqual(entry.dir, "/mnt/home/testuser") # Should use override with %u substitution
599+
600+
def testReadEntryWithHomeDirectoryOverrideNoSubstitution(self):
601+
"""Test _ReadEntry with home directory override without %u substitution."""
602+
self.mock_source.conf.update({
603+
"override_home_directory": "/shared/home"
604+
})
605+
user_data = {
606+
"id": "1001",
607+
"userName": "testuser",
608+
"homeDirectory": "/home/testuser",
609+
"loginShell": "/bin/bash",
610+
"name": {"formatted": "Test User"}
611+
}
612+
613+
entry = self.parser._ReadEntry(user_data)
614+
615+
self.assertIsNotNone(entry)
616+
self.assertEqual(entry.name, "testuser")
617+
self.assertEqual(entry.dir, "/shared/home") # Should use override as-is without substitution
618+
581619

582620
class TestScimSshkeyMapParser(unittest.TestCase):
583621
def setUp(self):

nsscache.conf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,12 @@ files_cache_filename_suffix = cache
155155
#
156156
# ldap_base = ou=people,dc=example,dc=com
157157

158+
# Override home directory for all users
159+
# If set, this takes precedence over ldap_home_dir
160+
# Use %%u to substitute the username (optional)
161+
# Examples:
162+
# ldap_override_home_dir = /mnt/home/%%u # Per-user: /mnt/home/john
163+
# ldap_override_home_dir = /shared/home # Same for all: /shared/home
158164
[group]
159165

160166
ldap_base = ou=group,dc=example,dc=com

nsscache.conf.5

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,14 @@ If specified, set every user's login shell to the given one. May be
218218
useful on bastion hosts or to ensure uniformity. Enable for
219219
Active Directory since the attribute (loginShell) is not default.
220220

221+
.TP
222+
.B ldap_override_home_dir
223+
If specified, set every user's home directory to the given value.
224+
Use %%u to substitute the username (optional). For example, /mnt/home/%%u would
225+
set user "john" to /mnt/home/john, while /shared/home would set all users
226+
to the same directory. This takes precedence over any
227+
home directory attributes and ldap_home_dir from the LDAP source.
228+
221229
.TP
222230
.B ldap_default_shell
223231
Set a default shell for all users if not specified.
@@ -389,6 +397,14 @@ Number of retries on soft failures before giving up. Defaults to 3.
389397
Default shell to assign to users if not specified in SCIM data. Defaults to
390398
.I /bin/bash
391399

400+
.TP
401+
.B scim_override_home_directory
402+
If specified in a [passwd] section, set every user's home directory to the
403+
given value. Use %%u to substitute the username (optional). For example,
404+
/mnt/home/%%u would set user "john" to /mnt/home/john, while /shared/home
405+
would set all users to the same directory. This takes precedence over any
406+
scim_path_home_directory configuration and SCIM response data.
407+
392408
The following path configuration options allow customization of how data is
393409
extracted from SCIM responses. These can be set per-map in
394410
[passwd], [group], or [sshkey] sections:

0 commit comments

Comments
 (0)