authentic/tests/test_commands.py

362 lines
13 KiB
Python

# authentic2 - versatile identity manager
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import datetime
import importlib
import json
import pytest
from django.contrib.contenttypes.models import ContentType
from django.utils import six
from django.utils.timezone import now
import py
from authentic2.a2_rbac.utils import get_default_ou
from authentic2.models import UserExternalId
from authentic2_auth_oidc.models import OIDCProvider, OIDCAccount
from django_rbac.models import ADMIN_OP
from django_rbac.models import Operation
from django_rbac.utils import get_operation
from django_rbac.utils import get_ou_model
from django_rbac.utils import get_permission_model
from django_rbac.utils import get_role_model
from django.contrib.auth import get_user_model
from .utils import login, call_command
User = get_user_model()
if six.PY2:
FileType = file # noqa: F821
else:
from io import TextIOWrapper, BufferedReader, BufferedWriter
FileType = (TextIOWrapper, BufferedReader, BufferedWriter)
def test_changepassword(db, simple_user, monkeypatch):
import getpass
def _getpass(*args, **kwargs):
return 'pass'
monkeypatch.setattr(getpass, 'getpass', _getpass)
call_command('changepassword', 'user')
old_pass = simple_user.password
simple_user.refresh_from_db()
assert old_pass != simple_user.password
def test_clean_unused_account(db, simple_user, mailoutbox, freezer, settings):
settings.LDAP_AUTH_SETTINGS = [{'realm': 'ldap', 'url': 'ldap://ldap.com/', 'basedn': 'dc=ldap,dc=com'}]
ldap_user = User.objects.create(username='ldap-user',
email='ldap-user@example.com',
ou=simple_user.ou)
oidc_user = User.objects.create(username='oidc-user',
email='oidc-user@example.com',
ou=simple_user.ou)
UserExternalId.objects.create(user=ldap_user, source='ldap',
external_id='whatever')
provider = OIDCProvider.objects.create(name='oidc', ou=simple_user.ou)
OIDCAccount.objects.create(user=oidc_user, provider=provider, sub='1')
email = simple_user.email
freezer.move_to('2018-01-01')
simple_user.ou.clean_unused_accounts_alert = 2
simple_user.ou.clean_unused_accounts_deletion = 3
simple_user.ou.save()
last_login = now() - datetime.timedelta(days=2, seconds=30)
for user in (simple_user, ldap_user, oidc_user):
user.last_login = last_login
user.save()
call_command('clean-unused-accounts')
for user in (simple_user, ldap_user, oidc_user):
user.refresh_from_db()
assert not simple_user.deleted
assert len(mailoutbox) == 1
freezer.move_to('2018-01-01 12:00:00')
# no new mail, no deletion
call_command('clean-unused-accounts')
for user in (simple_user, ldap_user, oidc_user):
user.refresh_from_db()
assert not simple_user.deleted
assert len(mailoutbox) == 1
freezer.move_to('2018-01-02')
call_command('clean-unused-accounts')
for user in (ldap_user, oidc_user):
user.refresh_from_db()
assert not simple_user.deleted
simple_user.refresh_from_db()
assert simple_user.deleted
assert len(mailoutbox) == 2
assert mailoutbox[-1].to == [email]
def test_clean_unused_account_user_logs_in(app, db, simple_user, mailoutbox, freezer):
freezer.move_to('2018-01-01')
simple_user.ou.clean_unused_accounts_alert = 2
simple_user.ou.clean_unused_accounts_deletion = 3
simple_user.ou.save()
simple_user.last_login = now() - datetime.timedelta(days=2)
simple_user.save()
call_command('clean-unused-accounts')
assert len(mailoutbox) == 1
login(app, simple_user)
# the day of deletion, nothing happens
freezer.move_to('2018-01-02')
simple_user.refresh_from_db()
assert not simple_user.deleted
assert len(mailoutbox) == 1
# when new alert delay is reached, user gets alerted again
freezer.move_to('2018-01-04')
call_command('clean-unused-accounts')
simple_user.refresh_from_db()
assert not simple_user.deleted
assert len(mailoutbox) == 2
def test_clean_unused_account_disabled_by_default(db, simple_user, mailoutbox):
simple_user.last_login = now() - datetime.timedelta(days=2)
simple_user.save()
call_command('clean-unused-accounts')
simple_user.refresh_from_db()
assert not simple_user.deleted
assert len(mailoutbox) == 0
def test_clean_unused_account_always_alert(db, simple_user, mailoutbox, freezer):
simple_user.ou.clean_unused_accounts_alert = 2
simple_user.ou.clean_unused_accounts_deletion = 3 # one day between alert and actual deletion
simple_user.ou.save()
simple_user.last_login = now() - datetime.timedelta(days=4)
simple_user.save()
# even if account last login in past deletion delay, an alert is always sent first
call_command('clean-unused-accounts')
simple_user.refresh_from_db()
assert not simple_user.deleted
assert len(mailoutbox) == 1
# and calling again as no effect, since one day must pass before account is deleted
call_command('clean-unused-accounts')
simple_user.refresh_from_db()
assert not simple_user.deleted
assert len(mailoutbox) == 1
@pytest.mark.parametrize("deletion_delay,formatted",
[(730, u'2\xa0years'), (500, u'1\xa0year'), (65, u'2\xa0months')])
def test_clean_unused_account_human_duration_format(simple_user, mailoutbox, deletion_delay, formatted):
simple_user.ou.clean_unused_accounts_alert = deletion_delay - 1
simple_user.ou.clean_unused_accounts_deletion = deletion_delay
simple_user.ou.save()
simple_user.last_login = now() - datetime.timedelta(days=deletion_delay + 1)
simple_user.save()
# alert email
call_command('clean-unused-accounts')
mail = mailoutbox[0]
assert formatted in mail.body
assert formatted in mail.subject and not '\n' in mail.subject
# deletion email
simple_user.last_account_deletion_alert = now() - datetime.timedelta(days=2)
simple_user.save()
call_command('clean-unused-accounts')
mail = mailoutbox[1]
assert formatted in mail.body
def test_clean_unused_account_login_url(simple_user, mailoutbox):
simple_user.ou.clean_unused_accounts_alert = 1
simple_user.ou.clean_unused_accounts_deletion = 2
simple_user.ou.save()
simple_user.last_login = now() - datetime.timedelta(days=1)
simple_user.save()
call_command('clean-unused-accounts')
mail = mailoutbox[0]
assert 'href="http://testserver/login/"' in mail.message().as_string()
def test_cleanupauthentic(db):
call_command('cleanupauthentic')
def test_load_ldif(db, monkeypatch, tmpdir):
ldif = tmpdir.join('some.ldif')
ldif.ensure()
class MockPArser(object):
def __init__(self, *args, **kwargs):
self.users = []
assert len(args) == 1
assert isinstance(args[0], FileType)
assert kwargs['options']['extra_attribute'] == {'ldap_attr': 'first_name'}
assert kwargs['options']['result'] == 'result'
def parse(self):
pass
oidc_cmd = importlib.import_module(
'authentic2.management.commands.load-ldif')
monkeypatch.setattr(oidc_cmd, 'DjangoUserLDIFParser', MockPArser)
call_command(
'load-ldif', ldif.strpath, result='result', extra_attribute={'ldap_attr': 'first_name'})
# test ExtraAttributeAction
class MockPArser(object):
def __init__(self, *args, **kwargs):
self.users = []
assert len(args) == 1
assert isinstance(args[0], FileType)
assert kwargs['options']['extra_attribute'] == {
'ldap_attr': 'first_name'}
assert kwargs['options']['result'] == 'result'
def parse(self):
pass
monkeypatch.setattr(oidc_cmd, 'DjangoUserLDIFParser', MockPArser)
call_command(
'load-ldif', '--extra-attribute', 'ldap_attr', 'first_name',
'--result', 'result', ldif.strpath)
def test_oidc_register_issuer(db, tmpdir, monkeypatch):
oidc_conf_f = py.path.local(__file__).dirpath('openid_configuration.json')
with oidc_conf_f.open() as f:
oidc_conf = json.load(f)
def register_issuer(
name, issuer=None, openid_configuration=None, verify=True, timeout=None,
ou=None):
OU = get_ou_model()
ou = OU.objects.get(default=True)
return OIDCProvider.objects.create(
name=name, ou=ou, issuer=issuer, strategy='create',
authorization_endpoint=openid_configuration['authorization_endpoint'],
token_endpoint=openid_configuration['token_endpoint'],
userinfo_endpoint=openid_configuration['userinfo_endpoint'],
end_session_endpoint=openid_configuration['end_session_endpoint'])
oidc_cmd = importlib.import_module(
'authentic2_auth_oidc.management.commands.oidc-register-issuer')
monkeypatch.setattr(oidc_cmd, 'register_issuer', register_issuer)
oidc_conf = py.path.local(__file__).dirpath('openid_configuration.json').strpath
call_command(
'oidc-register-issuer', '--openid-configuration', oidc_conf, '--issuer', 'issuer',
'somename')
provider = OIDCProvider.objects.get(name='somename')
assert provider.issuer == 'issuer'
def test_resetpassword(simple_user):
call_command('resetpassword', 'user')
old_pass = simple_user.password
simple_user.refresh_from_db()
assert old_pass != simple_user.password
def test_sync_metadata(db):
test_file = py.path.local(__file__).dirpath('metadata.xml').strpath
call_command('sync-metadata', test_file)
def test_check_and_repair_managers_of_roles(db, capsys):
Role = get_role_model()
Permission = get_permission_model()
default_ou = get_default_ou()
admin_op = get_operation(ADMIN_OP)
ou1 = get_ou_model().objects.create(name='Orgunit1', slug='orgunit1')
role1 = Role.objects.create(name='Role 1', slug='role-1', ou=default_ou)
perm1 = Permission.objects.create(
operation=admin_op, target_id=role1.id,
target_ct=ContentType.objects.get_for_model(Role))
manager_role1 = Role.objects.create(
name='Managers of Role 1', slug='_a2-managers-of-role-role1')
manager_role1.permissions.add(perm1)
manager_role1.save()
manager_role1_id = manager_role1.id
call_command('check-and-repair', '--repair', '--noinput')
captured = capsys.readouterr()
assert '"Managers of Role 1": no admin scope' in captured.out
assert 'Managers of Role 1" wrong ou, should be "Default organizational unit"' in captured.out
assert 'invalid permission "Management / role / Role 1": not manage_members operation' in captured.out
assert 'invalid permission "Management / role / Role 1": not admin_scope' in captured.out
assert 'invalid permission "Management / role / Role 1": wrong ou' in captured.out
def test_check_and_delete_unused_permissions(db, capsys, simple_user):
Permission = get_permission_model()
role1 = get_role_model().objects.create(name='Role1', slug='role1')
op1 = Operation.objects.create(name='Operation 1', slug='operation-1')
used_perm = Permission.objects.create(
operation=op1, target_id=role1.id,
target_ct=ContentType.objects.get_for_model(get_role_model()))
role1.admin_scope = used_perm
role1.save()
Permission.objects.create(
operation=op1, target_id=simple_user.id,
target_ct=ContentType.objects.get_for_model(get_user_model()))
call_command('check-and-repair', '--fake', '--noinput')
n_perm = len(Permission.objects.all())
call_command('check-and-repair', '--repair', '--noinput')
assert len(Permission.objects.all()) == n_perm - 1
def test_check_identifiers_uniqueness(db, capsys, settings):
settings.A2_EMAIL_IS_UNIQUE = False
settings.A2_USERNAME_IS_UNIQUE = False
user1 = User.objects.create(
username='foo', email='foo@example.net',
first_name='Toto', last_name='Foo')
user2 = User.objects.create(
username='foo', email='bar@example.net',
first_name='Bar', last_name='Foo')
user3 = User.objects.create(
username='bar', email='bar@example.net',
first_name='Tutu', last_name='Bar')
settings.A2_EMAIL_IS_UNIQUE = True
settings.A2_USERNAME_IS_UNIQUE = True
call_command('check-and-repair', '--repair', '--noinput')
captured = capsys.readouterr()
assert 'found 2 user accounts with same username' in captured.out
assert 'found 2 user accounts with same email' in captured.out