Skip to content

Commit c97ad0c

Browse files
committed
Add API to migrate from root user
This commit adds an API endpoint to migrate root user to one with the specified username and password combination. If password is omitted then the one currently used for root is preserved for the new admin account. Various root account parameters are migrated to the new admin account such as: * ssh keys * password enabled status * two factor authentication configuration and secret * email address * shell * home directory
1 parent 1127c9c commit c97ad0c

File tree

3 files changed

+153
-64
lines changed

3 files changed

+153
-64
lines changed

src/freenas/usr/local/bin/truenas-set-authentication-method.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44

55
import sqlite3
66

7-
from middlewared.plugins.account import ADMIN_UID, ADMIN_GID, crypted_password
7+
from middlewared.plugins.account import ADMIN_UID, ADMIN_GID
88
from middlewared.utils.db import FREENAS_DATABASE
99

1010
if __name__ == "__main__":
1111
authentication_method = json.loads(sys.stdin.read())
1212
username = authentication_method["username"]
13-
password = crypted_password(authentication_method["password"])
13+
password = authentication_method["password"]
1414

1515
conn = sqlite3.connect(FREENAS_DATABASE)
1616
conn.row_factory = sqlite3.Row

src/middlewared/middlewared/api/v25_04_0/user.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
"UserTwofactorConfigArgs", "UserTwofactorConfigResult",
3232
"UserVerifyTwofactorTokenArgs", "UserVerifyTwofactorTokenResult",
3333
"UserUnset2faSecretArgs", "UserUnset2faSecretResult",
34-
"UserRenew2faSecretArgs", "UserRenew2faSecretResult"]
34+
"UserRenew2faSecretArgs", "UserRenew2faSecretResult",
35+
"UserMigrateRootArgs", "UserMigrateRootResult",]
3536

3637

3738
DEFAULT_HOME_PATH = "/var/empty"
@@ -281,3 +282,17 @@ class UserRenew2faSecretArgs(BaseModel):
281282

282283

283284
UserRenew2faSecretResult = single_argument_result(UserEntry, "UserRenew2faSecretResult")
285+
286+
287+
@single_argument_args("migrate_root")
288+
class UserMigrateRootArgs(BaseModel):
289+
username: LocalUsername
290+
"""Username of new local user account to which to migration the root account.
291+
NOTE: user account nust not exist."""
292+
password: Secret[str | None] = None
293+
"""Password to set for new account. If password is unspecified then the password
294+
for the root account will be used."""
295+
296+
297+
class UserMigrateRootResult(BaseModel):
298+
result: None

src/middlewared/middlewared/plugins/account.py

Lines changed: 135 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import shlex
77
import shutil
88
import stat
9+
import subprocess
910
import wbclient
1011
from pathlib import Path
1112
from collections import defaultdict
@@ -46,6 +47,8 @@
4647
UserShellChoicesResult,
4748
UserUpdateArgs,
4849
UserUpdateResult,
50+
UserMigrateRootArgs,
51+
UserMigrateRootResult
4952
)
5053
from middlewared.service import CallError, CRUDService, ValidationErrors, pass_app, private, job
5154
from middlewared.service_exception import MatchNotFound
@@ -1149,6 +1152,132 @@ async def has_local_administrator_set_up(self):
11491152
"""
11501153
return len(await self.middleware.call('privilege.local_administrators')) > 0
11511154

1155+
@api_method(
1156+
UserMigrateRootArgs, UserMigrateRootResult,
1157+
roles=['ACCOUNT_WRITE'], audit='Migrate root account'
1158+
)
1159+
@job(lock='migrate_root')
1160+
def migrate_root(self, job, data):
1161+
"""
1162+
Migrate from root user account to new one with UID 950 and the specified
1163+
`username`. If this account already exists then we consider migration to
1164+
have already happened and will fail with CallError and errno set to EEXIST.
1165+
"""
1166+
username = data['username']
1167+
verrors = ValidationErrors()
1168+
pw_checkname(verrors, 'account_migrate_root.username', username)
1169+
verrors.check()
1170+
1171+
root_user = self.middleware.call_sync('user.query', [['uid', '=', 0]], {'get': True})
1172+
homedir = f'/home/{username}'
1173+
1174+
if data['password'] is not None:
1175+
password_hash = crypted_password(data['password'])
1176+
else:
1177+
password_hash = root_user['unixhash']
1178+
1179+
try:
1180+
pwd_obj = self.middleware.call_sync('user.get_user_obj', {'uid': ADMIN_UID})
1181+
raise CallError(
1182+
f'A {pwd_obj["source"].lower()} user with uid={ADMIN_UID} already exists, '
1183+
'setting up local administrator is not possible',
1184+
errno.EEXIST,
1185+
)
1186+
except KeyError:
1187+
pass
1188+
1189+
try:
1190+
pwd_obj = self.middleware.call_sync('user.get_user_obj', {'username': username})
1191+
raise CallError(f'{username!r} {pwd_obj["source"].lower()} user already exists, '
1192+
'setting up local administrator is not possible',
1193+
errno.EEXIST)
1194+
except KeyError:
1195+
pass
1196+
1197+
try:
1198+
grp_obj = self.middleware.call_sync('group.get_group_obj', {'gid': ADMIN_GID})
1199+
raise CallError(
1200+
f'A {grp_obj["source"].lower()} group with gid={ADMIN_GID} already exists, '
1201+
'setting up local administrator is not possible',
1202+
errno.EEXIST,
1203+
)
1204+
except KeyError:
1205+
pass
1206+
1207+
try:
1208+
grp_obj = self.middleware.call_sync('group.get_group_obj', {'groupname': username})
1209+
raise CallError(f'{username!r} {grp_obj["source"].lower()} group already exists, '
1210+
'setting up local administrator is not possible',
1211+
errno.EEXIST)
1212+
except KeyError:
1213+
pass
1214+
1215+
# double-check our database in case we have for some reason failed to write to passwd
1216+
local_users = self.middleware.call_sync('user.query', [['local', '=', True]])
1217+
local_groups = self.middleware.call_sync('group.query', [['local', '=', True]])
1218+
1219+
if filter_list(local_users, [['uid', '=', ADMIN_UID]]):
1220+
raise CallError(
1221+
f'A user with uid={ADMIN_UID} already exists, setting up local administrator is not possible',
1222+
errno.EEXIST,
1223+
)
1224+
1225+
if filter_list(local_users, [['username', '=', username]]):
1226+
raise CallError(f'{username!r} user already exists, setting up local administrator is not possible',
1227+
errno.EEXIST)
1228+
1229+
if filter_list(local_groups, [['gid', '=', ADMIN_GID]]):
1230+
raise CallError(
1231+
f'A group with gid={ADMIN_GID} already exists, setting up local administrator is not possible',
1232+
errno.EEXIST,
1233+
)
1234+
1235+
if filter_list(local_groups, [['group', '=', username]]):
1236+
raise CallError(f'{username!r} group already exists, setting up local administrator is not possible',
1237+
errno.EEXIST)
1238+
1239+
subprocess.run(
1240+
['truenas-set-authentication-method.py'],
1241+
check=True, encoding='utf-8', errors='ignore',
1242+
input=json.dumps({'username': username, 'password': password_hash})
1243+
)
1244+
new_user = self.middleware.call_sync('user.query', [['uid', '=', ADMIN_UID]], {'get': True})
1245+
1246+
self.middleware.call_sync('failover.datastore.force_send')
1247+
self.middleware.call_sync('etc.generate', 'user')
1248+
1249+
# Set up homedir for new admin user
1250+
try:
1251+
os.mkdir(homedir, 0o700)
1252+
except FileExistsError:
1253+
pass
1254+
1255+
os.chown(homedir, ADMIN_UID, ADMIN_GID)
1256+
os.chmod(homedir, 0o700)
1257+
home_copy_job = self.middleware.call_sync('user.do_home_copy', '/root', homedir, '700', ADMIN_UID)
1258+
home_copy_job.wait_sync()
1259+
1260+
# Update new user account with settings from root
1261+
self.middleware.call_sync('user.update', new_user['id'], {
1262+
'ssh_password_enabled': root_user['ssh_password_enabled'],
1263+
'sshpubkey': root_user['sshpubkey'],
1264+
'email': root_user['email'],
1265+
'shell': root_user['shell'],
1266+
})
1267+
1268+
# Preserve root twofactor settings
1269+
if root_user['twofactor_auth_configured']:
1270+
# get twofactor config for UID 0 and copy it over to 950
1271+
twofactor_data = self.middleware.call_sync('datastore.query', 'account.twofactor_user_auth')
1272+
root_twofactor = filter_list(twofactor_data, [['user.bsdusr_uid', '=', 0]], {'get': True})
1273+
target = filter_list(twofactor_data, [['user.bsdusr_uid', '=', ADMIN_UID]], {'get': True})['id']
1274+
1275+
self.middleware.call_sync('datastore.update', 'account.twofactor_user_auth', target, {
1276+
'secret': root_twofactor['secret'],
1277+
'otp_digits': root_twofactor['otp_digits'],
1278+
'interval': root_twofactor['interval'],
1279+
})
1280+
11521281
@api_method(
11531282
UserSetupLocalAdministratorArgs, UserSetupLocalAdministratorResult,
11541283
audit='Set up local administrator',
@@ -1164,69 +1293,14 @@ async def setup_local_administrator(self, app, username, password, options):
11641293
raise CallError('Local administrator is already set up', errno.EEXIST)
11651294

11661295
if username == 'truenas_admin':
1167-
# first check based on NSS to catch collisions with AD / LDAP users
1168-
try:
1169-
pwd_obj = await self.middleware.call('user.get_user_obj', {'uid': ADMIN_UID})
1170-
raise CallError(
1171-
f'A {pwd_obj["source"].lower()} user with uid={ADMIN_UID} already exists, '
1172-
'setting up local administrator is not possible',
1173-
errno.EEXIST,
1174-
)
1175-
except KeyError:
1176-
pass
1177-
1178-
try:
1179-
pwd_obj = await self.middleware.call('user.get_user_obj', {'username': username})
1180-
raise CallError(f'{username!r} {pwd_obj["source"].lower()} user already exists, '
1181-
'setting up local administrator is not possible',
1182-
errno.EEXIST)
1183-
except KeyError:
1184-
pass
1185-
1186-
try:
1187-
grp_obj = await self.middleware.call('group.get_group_obj', {'gid': ADMIN_GID})
1188-
raise CallError(
1189-
f'A {grp_obj["source"].lower()} group with gid={ADMIN_GID} already exists, '
1190-
'setting up local administrator is not possible',
1191-
errno.EEXIST,
1192-
)
1193-
except KeyError:
1194-
pass
1195-
1196-
try:
1197-
grp_obj = await self.middleware.call('group.get_group_obj', {'groupname': username})
1198-
raise CallError(f'{username!r} {grp_obj["source"].lower()} group already exists, '
1199-
'setting up local administrator is not possible',
1200-
errno.EEXIST)
1201-
except KeyError:
1202-
pass
1203-
1204-
# double-check our database in case we have for some reason failed to write to passwd
1205-
local_users = await self.middleware.call('user.query', [['local', '=', True]])
1206-
local_groups = await self.middleware.call('group.query', [['local', '=', True]])
1207-
1208-
if filter_list(local_users, [['uid', '=', ADMIN_UID]]):
1209-
raise CallError(
1210-
f'A user with uid={ADMIN_UID} already exists, setting up local administrator is not possible',
1211-
errno.EEXIST,
1212-
)
1213-
1214-
if filter_list(local_users, [['username', '=', username]]):
1215-
raise CallError(f'{username!r} user already exists, setting up local administrator is not possible',
1216-
errno.EEXIST)
1217-
1218-
if filter_list(local_groups, [['gid', '=', ADMIN_GID]]):
1219-
raise CallError(
1220-
f'A group with gid={ADMIN_GID} already exists, setting up local administrator is not possible',
1221-
errno.EEXIST,
1222-
)
1223-
1224-
if filter_list(local_groups, [['group', '=', username]]):
1225-
raise CallError(f'{username!r} group already exists, setting up local administrator is not possible',
1226-
errno.EEXIST)
1296+
# This should be relatively invexpensive even though it's a job since we
1297+
# don't expect /root to have much in the way of contents.
1298+
migrate_job = await self.middleware.call('user.migrate_root', {'username': username, 'password': password})
1299+
await migrate_job.wait(raise_error=True)
1300+
return
12271301

12281302
await run('truenas-set-authentication-method.py', check=True, encoding='utf-8', errors='ignore',
1229-
input=json.dumps({'username': username, 'password': password}))
1303+
input=json.dumps({'username': username, 'password': crypted_password(password)}))
12301304
await self.middleware.call('failover.datastore.force_send')
12311305
await self.middleware.call('etc.generate', 'user')
12321306

0 commit comments

Comments
 (0)