mirror of
https://github.com/inventree/InvenTree.git
synced 2026-01-28 09:03:41 +00:00
feat(backend): enable reseting mfa via username from the cli (#11133)
* feat(backend): enable reseting mfa via username * fix tests * extend testing saveguards to username cli
This commit is contained in:
@@ -13,33 +13,59 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
"""Add the arguments."""
|
"""Add the arguments."""
|
||||||
parser.add_argument('mail', type=str)
|
parser.add_argument('--mail', type=str, nargs='?')
|
||||||
|
parser.add_argument('--username', type=str, nargs='?')
|
||||||
|
|
||||||
def handle(self, *args, mail, **kwargs):
|
def handle(self, *args, mail, username, **kwargs):
|
||||||
"""Remove MFA for the supplied user (by mail)."""
|
"""Remove MFA for the supplied user (by mail or username)."""
|
||||||
user = get_user_model()
|
user = get_user_model()
|
||||||
mfa_user = [
|
mfa_user = []
|
||||||
*set(
|
success = False
|
||||||
user.objects.filter(email=mail)
|
|
||||||
| user.objects.filter(emailaddress__email=mail)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
if len(mfa_user) == 0:
|
if mail is not None:
|
||||||
logger.warning('No user with this mail associated')
|
mfa_user = [
|
||||||
elif len(mfa_user) > 1:
|
*set(
|
||||||
emails_list = ', '.join(
|
user.objects.filter(email=mail)
|
||||||
sorted(
|
| user.objects.filter(emailaddress__email=mail)
|
||||||
{b.email for a in mfa_user for b in a.emailaddress_set.all()}
|
|
||||||
| {a.email for a in mfa_user}
|
|
||||||
)
|
)
|
||||||
)
|
]
|
||||||
usernames_list = ', '.join(sorted({a.username for a in mfa_user}))
|
if len(mfa_user) == 0:
|
||||||
logger.error(
|
logger.warning('No user with this mail associated')
|
||||||
f"Multiple users found with the provided email; Usernames: '{usernames_list}', Emails: '{emails_list}'"
|
elif len(mfa_user) > 1:
|
||||||
)
|
emails_list = ', '.join(
|
||||||
|
sorted(
|
||||||
|
{b.email for a in mfa_user for b in a.emailaddress_set.all()}
|
||||||
|
| {a.email for a in mfa_user}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
usernames_list = ', '.join(sorted({a.username for a in mfa_user}))
|
||||||
|
logger.error(
|
||||||
|
f"Multiple users found with the provided email; Usernames: '{usernames_list}', Emails: '{emails_list}'"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# found exactly one user
|
||||||
|
success = True
|
||||||
|
|
||||||
|
elif username is not None:
|
||||||
|
mfa_user = user.objects.filter(username=username)
|
||||||
|
if len(mfa_user) == 0:
|
||||||
|
logger.warning('No user with this username associated')
|
||||||
|
elif (
|
||||||
|
len(mfa_user) > 1
|
||||||
|
): # pragma: no cover # Should not be possible due to unique constraint
|
||||||
|
logger.error('Multiple users found with the provided username')
|
||||||
|
else:
|
||||||
|
# found exactly one user
|
||||||
|
success = True
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# and clean out all MFA methods
|
logger.error('No mail or username provided')
|
||||||
|
raise ValueError(
|
||||||
|
'Error: one of the following arguments is required: mail, username'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clean out all MFA methods
|
||||||
|
if success:
|
||||||
auths = mfa_user[0].authenticator_set.all()
|
auths = mfa_user[0].authenticator_set.all()
|
||||||
length = len(auths)
|
length = len(auths)
|
||||||
auths.delete()
|
auths.delete()
|
||||||
|
|||||||
@@ -19,40 +19,50 @@ class CommandTestCase(TestCase):
|
|||||||
|
|
||||||
def test_remove_mfa(self):
|
def test_remove_mfa(self):
|
||||||
"""Test the remove_mfa command."""
|
"""Test the remove_mfa command."""
|
||||||
|
|
||||||
|
def get_dummyuser(uname='admin'):
|
||||||
|
admin = User.objects.create_user(
|
||||||
|
username=uname, email=f'{uname}@example.org'
|
||||||
|
)
|
||||||
|
admin.authenticator_set.create(type='TOTP', data={})
|
||||||
|
self.assertEqual(admin.authenticator_set.all().count(), 1)
|
||||||
|
return admin
|
||||||
|
|
||||||
# missing arg
|
# missing arg
|
||||||
with self.assertRaises(Exception) as cm:
|
with self.assertRaises(Exception) as cm:
|
||||||
call_command('remove_mfa', verbosity=0)
|
call_command('remove_mfa', verbosity=0)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
'Error: the following arguments are required: mail', str(cm.exception)
|
'Error: one of the following arguments is required: mail, username',
|
||||||
|
str(cm.exception),
|
||||||
)
|
)
|
||||||
|
|
||||||
# no user
|
# no user
|
||||||
with self.assertLogs('inventree') as cm:
|
with self.assertLogs('inventree') as cm:
|
||||||
self.assertFalse(
|
self.assertFalse(
|
||||||
call_command('remove_mfa', 'admin@example.org', verbosity=0)
|
call_command('remove_mfa', mail='admin@example.org', verbosity=0)
|
||||||
)
|
)
|
||||||
self.assertIn('No user with this mail associated', str(cm[1]))
|
self.assertIn('No user with this mail associated', str(cm[1]))
|
||||||
|
|
||||||
# correct removal
|
# correct removal
|
||||||
my_admin1 = User.objects.create_user(
|
my_admin1 = get_dummyuser()
|
||||||
username='admin', email='admin@example.org'
|
output = call_command('remove_mfa', mail=my_admin1.email, verbosity=0)
|
||||||
)
|
|
||||||
my_admin1.authenticator_set.create(type='TOTP', data={})
|
|
||||||
self.assertEqual(my_admin1.authenticator_set.all().count(), 1)
|
|
||||||
output = call_command('remove_mfa', 'admin@example.org', verbosity=0)
|
|
||||||
self.assertEqual(output, 'done')
|
self.assertEqual(output, 'done')
|
||||||
self.assertEqual(my_admin1.authenticator_set.all().count(), 0)
|
self.assertEqual(my_admin1.authenticator_set.all().count(), 0)
|
||||||
|
|
||||||
# two users with same email
|
# two users with same email
|
||||||
my_admin2 = User.objects.create_user(
|
my_admin2 = User.objects.create_user(username='admin2', email=my_admin1.email)
|
||||||
username='admin2', email='admin@example.org'
|
|
||||||
)
|
|
||||||
my_admin2.emailaddress_set.create(email='456')
|
my_admin2.emailaddress_set.create(email='456')
|
||||||
my_admin2.emailaddress_set.create(email='123')
|
my_admin2.emailaddress_set.create(email='123')
|
||||||
with self.assertLogs('inventree') as cm:
|
with self.assertLogs('inventree') as cm:
|
||||||
self.assertFalse(
|
self.assertFalse(
|
||||||
call_command('remove_mfa', 'admin@example.org', verbosity=0)
|
call_command('remove_mfa', mail=my_admin1.email, verbosity=0)
|
||||||
)
|
)
|
||||||
self.assertIn('Multiple users found with the provided email', str(cm[1]))
|
self.assertIn('Multiple users found with the provided email', str(cm[1]))
|
||||||
self.assertIn('admin, admin2', str(cm[1]))
|
self.assertIn('admin, admin2', str(cm[1]))
|
||||||
self.assertIn('123, 456, admin@example.org', str(cm[1]))
|
self.assertIn(f'123, 456, {my_admin1.email}', str(cm[1]))
|
||||||
|
|
||||||
|
# correct removal by username
|
||||||
|
my_admin3 = get_dummyuser('admin3')
|
||||||
|
output = call_command('remove_mfa', username=my_admin3.username, verbosity=0)
|
||||||
|
self.assertEqual(output, 'done')
|
||||||
|
self.assertEqual(my_admin3.authenticator_set.all().count(), 0)
|
||||||
|
|||||||
15
tasks.py
15
tasks.py
@@ -619,14 +619,19 @@ def clean_settings(c):
|
|||||||
success('Settings cleaned successfully')
|
success('Settings cleaned successfully')
|
||||||
|
|
||||||
|
|
||||||
@task(help={'mail': "mail of the user who's MFA should be disabled"})
|
@task(
|
||||||
def remove_mfa(c, mail=''):
|
help={
|
||||||
|
'mail': "mail of the user who's MFA should be disabled",
|
||||||
|
'username': "username of the user who's MFA should be disabled",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def remove_mfa(c, mail='', username=''):
|
||||||
"""Remove MFA for a user."""
|
"""Remove MFA for a user."""
|
||||||
if not mail:
|
if not mail and not username:
|
||||||
warning('You must provide a users mail')
|
warning('You must provide a users mail or username')
|
||||||
return
|
return
|
||||||
|
|
||||||
manage(c, f'remove_mfa {mail}')
|
manage(c, f'remove_mfa --mail {mail} --username {username}')
|
||||||
|
|
||||||
|
|
||||||
@task(
|
@task(
|
||||||
|
|||||||
Reference in New Issue
Block a user