Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • Dockerfile
  • feature_invite_validuntil_minmax
  • incremental-sync
  • jwt_encode_inconsistencies
  • master
  • redis-rate-limits
  • roles-recursive-cte
  • typehints
  • v1.0.x
  • v1.1.x
  • v1.2.x
  • v1.x.x
  • v0.1.2
  • v0.1.4
  • v0.1.5
  • v0.2.0
  • v0.3.0
  • v1.0.0
  • v1.0.1
  • v1.0.2
  • v1.1.0
  • v1.1.1
  • v1.1.2
  • v1.2.0
  • v2.0.0
  • v2.0.1
  • v2.1.0
  • v2.2.0
  • v2.3.0
  • v2.3.1
30 results

Target

Select target project
  • uffd/uffd
  • rixx/uffd
  • thies/uffd
  • leona/uffd
  • enbewe/uffd
  • strifel/uffd
  • thies/uffd-2
7 results
Select Git revision
  • Dockerfile
  • claims-in-idtoke
  • feature_invite_validuntil_minmax
  • incremental-sync
  • jwt_encode_inconsistencies
  • master
  • recovery-code-pwhash
  • redis-rate-limits
  • roles-recursive-cte
  • typehints
  • v1.0.x
  • v1.1.x
  • v1.2.x
  • v1.x.x
  • v0.1.2
  • v0.1.4
  • v0.1.5
  • v0.2.0
  • v0.3.0
  • v1.0.0
  • v1.0.1
  • v1.0.2
  • v1.1.0
  • v1.1.1
  • v1.1.2
  • v1.2.0
  • v2.0.0
  • v2.0.1
  • v2.1.0
  • v2.2.0
  • v2.3.0
  • v2.3.1
32 results
Show changes
Showing
with 2786 additions and 437 deletions
from flask import url_for
from uffd.database import db
from uffd.models import User, Role, RoleGroup
from tests.utils import dump, UffdTestCase
class TestRoleViews(UffdTestCase):
def setUp(self):
super().setUp()
self.login_as('admin')
def test_index(self):
db.session.add(Role(name='base', description='Base role description'))
db.session.add(Role(name='test1', description='Test1 role description'))
db.session.commit()
r = self.client.get(path=url_for('role.index'), follow_redirects=True)
dump('role_index', r)
self.assertEqual(r.status_code, 200)
def test_index_empty(self):
r = self.client.get(path=url_for('role.index'), follow_redirects=True)
dump('role_index_empty', r)
self.assertEqual(r.status_code, 200)
def test_show(self):
role = Role(name='base', description='Base role description')
db.session.add(role)
db.session.commit()
r = self.client.get(path=url_for('role.show', roleid=role.id), follow_redirects=True)
dump('role_show', r)
self.assertEqual(r.status_code, 200)
def test_new(self):
r = self.client.get(path=url_for('role.new'), follow_redirects=True)
dump('role_new', r)
self.assertEqual(r.status_code, 200)
def test_update(self):
role = Role(name='base', description='Base role description')
db.session.add(role)
db.session.commit()
role.groups[self.get_admin_group()] = RoleGroup()
db.session.commit()
self.assertEqual(role.name, 'base')
self.assertEqual(role.description, 'Base role description')
self.assertSetEqual(set(role.groups), {self.get_admin_group()})
r = self.client.post(path=url_for('role.update', roleid=role.id),
data={'name': 'base1', 'description': 'Base role description1', 'moderator-group': '', 'group-%d'%self.get_users_group().id: '1', 'group-%d'%self.get_access_group().id: '1'},
follow_redirects=True)
dump('role_update', r)
self.assertEqual(r.status_code, 200)
role = Role.query.get(role.id)
self.assertEqual(role.name, 'base1')
self.assertEqual(role.description, 'Base role description1')
self.assertSetEqual(set(role.groups), {self.get_access_group(), self.get_users_group()})
# TODO: verify that group memberships are updated
def test_create(self):
self.assertIsNone(Role.query.filter_by(name='base').first())
r = self.client.post(path=url_for('role.update'),
data={'name': 'base', 'description': 'Base role description', 'moderator-group': '', 'group-%d'%self.get_users_group().id: '1', 'group-%d'%self.get_access_group().id: '1'},
follow_redirects=True)
dump('role_create', r)
self.assertEqual(r.status_code, 200)
role = Role.query.filter_by(name='base').first()
self.assertIsNotNone(role)
self.assertEqual(role.name, 'base')
self.assertEqual(role.description, 'Base role description')
self.assertSetEqual(set(role.groups), {self.get_access_group(), self.get_users_group()})
# TODO: verify that group memberships are updated (currently not possible with ldap mock!)
def test_create_with_moderator_group(self):
self.assertIsNone(Role.query.filter_by(name='base').first())
r = self.client.post(path=url_for('role.update'),
data={'name': 'base', 'description': 'Base role description', 'moderator-group': self.get_admin_group().id, 'group-%d'%self.get_users_group().id: '1', 'group-%d'%self.get_access_group().id: '1'},
follow_redirects=True)
self.assertEqual(r.status_code, 200)
role = Role.query.filter_by(name='base').first()
self.assertIsNotNone(role)
self.assertEqual(role.name, 'base')
self.assertEqual(role.description, 'Base role description')
self.assertEqual(role.moderator_group.name, 'uffd_admin')
self.assertSetEqual(set(role.groups), {self.get_access_group(), self.get_users_group()})
# TODO: verify that group memberships are updated (currently not possible with ldap mock!)
def test_delete(self):
role = Role(name='base', description='Base role description')
db.session.add(role)
db.session.commit()
role_id = role.id
self.assertIsNotNone(Role.query.get(role_id))
r = self.client.get(path=url_for('role.delete', roleid=role.id), follow_redirects=True)
dump('role_delete', r)
self.assertEqual(r.status_code, 200)
self.assertIsNone(Role.query.get(role_id))
# TODO: verify that group memberships are updated (currently not possible with ldap mock!)
def test_set_default(self):
db.session.add(User(loginname='service', is_service_user=True, primary_email_address='service@example.com', displayname='Service'))
db.session.commit()
role = Role(name='test')
db.session.add(role)
role.groups[self.get_admin_group()] = RoleGroup()
user1 = self.get_user()
user2 = self.get_admin()
service_user = User.query.filter_by(loginname='service').one_or_none()
self.assertSetEqual(set(self.get_user().roles_effective), set())
self.assertSetEqual(set(self.get_admin().roles_effective), set())
self.assertSetEqual(set(service_user.roles_effective), set())
role.members.append(self.get_user())
role.members.append(service_user)
self.assertSetEqual(set(self.get_user().roles_effective), {role})
self.assertSetEqual(set(self.get_admin().roles_effective), set())
self.assertSetEqual(set(service_user.roles_effective), {role})
db.session.commit()
role_id = role.id
self.assertSetEqual(set(role.members), {self.get_user(), service_user})
r = self.client.get(path=url_for('role.set_default', roleid=role.id), follow_redirects=True)
dump('role_set_default', r)
self.assertEqual(r.status_code, 200)
role = Role.query.get(role_id)
service_user = User.query.filter_by(loginname='service').one_or_none()
self.assertSetEqual(set(role.members), {service_user})
self.assertSetEqual(set(self.get_user().roles_effective), {role})
self.assertSetEqual(set(self.get_admin().roles_effective), {role})
def test_unset_default(self):
admin_role = Role(name='admin', is_default=True)
db.session.add(admin_role)
admin_role.groups[self.get_admin_group()] = RoleGroup()
db.session.add(User(loginname='service', is_service_user=True, primary_email_address='service@example.com', displayname='Service'))
db.session.commit()
role = Role(name='test', is_default=True)
db.session.add(role)
service_user = User.query.filter_by(loginname='service').one_or_none()
role.members.append(service_user)
self.assertSetEqual(set(self.get_user().roles_effective), {role, admin_role})
self.assertSetEqual(set(self.get_admin().roles_effective), {role, admin_role})
self.assertSetEqual(set(service_user.roles_effective), {role})
db.session.commit()
role_id = role.id
admin_role_id = admin_role.id
self.assertSetEqual(set(role.members), {service_user})
r = self.client.get(path=url_for('role.unset_default', roleid=role.id), follow_redirects=True)
dump('role_unset_default', r)
self.assertEqual(r.status_code, 200)
role = Role.query.get(role_id)
admin_role = Role.query.get(admin_role_id)
service_user = User.query.filter_by(loginname='service').one_or_none()
self.assertSetEqual(set(role.members), {service_user})
self.assertSetEqual(set(self.get_user().roles_effective), {admin_role})
self.assertSetEqual(set(self.get_admin().roles_effective), {admin_role})
from flask import url_for from flask import url_for
from uffd.user.models import User, Group
from uffd.role.models import Role, RoleGroup
from uffd.database import db from uffd.database import db
from uffd.ldap import ldap from uffd.models import Role, RoleGroup
from utils import dump, UffdTestCase from tests.utils import dump, UffdTestCase
class TestRolemodViewsLoggedOut(UffdTestCase): class TestRolemodViewsLoggedOut(UffdTestCase):
def test_acl_nologin(self): def test_acl_nologin(self):
...@@ -36,13 +34,12 @@ class TestRolemodViews(UffdTestCase): ...@@ -36,13 +34,12 @@ class TestRolemodViews(UffdTestCase):
db.session.commit() db.session.commit()
r = self.client.get(path=url_for('rolemod.index'), follow_redirects=True) r = self.client.get(path=url_for('rolemod.index'), follow_redirects=True)
dump('rolemod_acl_notmod', r) dump('rolemod_acl_notmod', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 403)
self.assertIn('Access denied'.encode(), r.data)
def test_show(self): def test_show(self):
role = Role(name='test', moderator_group=self.get_access_group()) role = Role(name='test', moderator_group=self.get_access_group())
db.session.add(role) db.session.add(role)
role.members.add(self.get_admin()) role.members.append(self.get_admin())
db.session.commit() db.session.commit()
r = self.client.get(path=url_for('rolemod.show', role_id=role.id), follow_redirects=True) r = self.client.get(path=url_for('rolemod.show', role_id=role.id), follow_redirects=True)
dump('rolemod_show', r) dump('rolemod_show', r)
...@@ -64,7 +61,7 @@ class TestRolemodViews(UffdTestCase): ...@@ -64,7 +61,7 @@ class TestRolemodViews(UffdTestCase):
db.session.commit() db.session.commit()
r = self.client.get(path=url_for('rolemod.show', role_id=role.id), follow_redirects=True) r = self.client.get(path=url_for('rolemod.show', role_id=role.id), follow_redirects=True)
dump('rolemod_show_noperm', r) dump('rolemod_show_noperm', r)
self.assertIn('Access denied'.encode(), r.data) self.assertEqual(r.status_code, 403)
def test_show_nomod(self): def test_show_nomod(self):
# Make sure we pass the blueprint-wide acl check # Make sure we pass the blueprint-wide acl check
...@@ -74,7 +71,7 @@ class TestRolemodViews(UffdTestCase): ...@@ -74,7 +71,7 @@ class TestRolemodViews(UffdTestCase):
db.session.commit() db.session.commit()
r = self.client.get(path=url_for('rolemod.show', role_id=role.id), follow_redirects=True) r = self.client.get(path=url_for('rolemod.show', role_id=role.id), follow_redirects=True)
dump('rolemod_show_nomod', r) dump('rolemod_show_nomod', r)
self.assertIn('Access denied'.encode(), r.data) self.assertEqual(r.status_code, 403)
def test_update(self): def test_update(self):
role = Role(name='test', description='old_description', moderator_group=self.get_access_group()) role = Role(name='test', description='old_description', moderator_group=self.get_access_group())
...@@ -102,7 +99,7 @@ class TestRolemodViews(UffdTestCase): ...@@ -102,7 +99,7 @@ class TestRolemodViews(UffdTestCase):
db.session.commit() db.session.commit()
r = self.client.post(path=url_for('rolemod.update', role_id=role.id), data={'description': 'new_description'}, follow_redirects=True) r = self.client.post(path=url_for('rolemod.update', role_id=role.id), data={'description': 'new_description'}, follow_redirects=True)
dump('rolemod_update_noperm', r) dump('rolemod_update_noperm', r)
self.assertIn('Access denied'.encode(), r.data) self.assertEqual(r.status_code, 403)
self.assertEqual(Role.query.get(role.id).description, 'old_description') self.assertEqual(Role.query.get(role.id).description, 'old_description')
def test_update_nomod(self): def test_update_nomod(self):
...@@ -113,23 +110,23 @@ class TestRolemodViews(UffdTestCase): ...@@ -113,23 +110,23 @@ class TestRolemodViews(UffdTestCase):
db.session.commit() db.session.commit()
r = self.client.post(path=url_for('rolemod.update', role_id=role.id), data={'description': 'new_description'}, follow_redirects=True) r = self.client.post(path=url_for('rolemod.update', role_id=role.id), data={'description': 'new_description'}, follow_redirects=True)
dump('rolemod_update_nomod', r) dump('rolemod_update_nomod', r)
self.assertIn('Access denied'.encode(), r.data) self.assertEqual(r.status_code, 403)
self.assertEqual(Role.query.get(role.id).description, 'old_description') self.assertEqual(Role.query.get(role.id).description, 'old_description')
def test_delete_member(self): def test_delete_member(self):
role = Role(name='test', moderator_group=self.get_access_group()) role = Role(name='test', moderator_group=self.get_access_group())
role.groups[self.get_admin_group()] = RoleGroup() role.groups[self.get_admin_group()] = RoleGroup()
db.session.add(role) db.session.add(role)
role.members.add(self.get_admin()) role.members.append(self.get_admin())
db.session.commit() db.session.commit()
role.update_member_groups() role.update_member_groups()
ldap.session.commit() db.session.commit()
user = self.get_admin() user = self.get_admin()
group = self.get_admin_group() group = self.get_admin_group()
self.assertTrue(user in group.members) self.assertTrue(user in group.members)
role = Role.query.get(role.id) role = Role.query.get(role.id)
self.assertTrue(user in role.members) self.assertTrue(user in role.members)
r = self.client.get(path=url_for('rolemod.delete_member', role_id=role.id, member_dn=user.dn), follow_redirects=True) r = self.client.get(path=url_for('rolemod.delete_member', role_id=role.id, member_id=user.id), follow_redirects=True)
dump('rolemod_delete_member', r) dump('rolemod_delete_member', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
user_updated = self.get_admin() user_updated = self.get_admin()
...@@ -144,7 +141,7 @@ class TestRolemodViews(UffdTestCase): ...@@ -144,7 +141,7 @@ class TestRolemodViews(UffdTestCase):
db.session.add(role) db.session.add(role)
db.session.commit() db.session.commit()
user = self.get_admin() user = self.get_admin()
r = self.client.get(path=url_for('rolemod.delete_member', role_id=role.id, member_dn=user.dn), follow_redirects=True) r = self.client.get(path=url_for('rolemod.delete_member', role_id=role.id, member_id=user.id), follow_redirects=True)
dump('rolemod_delete_member_nomember', r) dump('rolemod_delete_member_nomember', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
...@@ -153,14 +150,14 @@ class TestRolemodViews(UffdTestCase): ...@@ -153,14 +150,14 @@ class TestRolemodViews(UffdTestCase):
db.session.add(Role(name='other_role', moderator_group=self.get_access_group())) db.session.add(Role(name='other_role', moderator_group=self.get_access_group()))
role = Role(name='test', moderator_group=self.get_admin_group()) role = Role(name='test', moderator_group=self.get_admin_group())
db.session.add(role) db.session.add(role)
role.members.add(self.get_admin()) role.members.append(self.get_admin())
db.session.commit() db.session.commit()
user = self.get_admin() user = self.get_admin()
role = Role.query.get(role.id) role = Role.query.get(role.id)
self.assertTrue(user in role.members) self.assertTrue(user in role.members)
r = self.client.get(path=url_for('rolemod.delete_member', role_id=role.id, member_dn=user.dn), follow_redirects=True) r = self.client.get(path=url_for('rolemod.delete_member', role_id=role.id, member_id=user.id), follow_redirects=True)
dump('rolemod_delete_member_noperm', r) dump('rolemod_delete_member_noperm', r)
self.assertIn('Access denied'.encode(), r.data) self.assertEqual(r.status_code, 403)
user_updated = self.get_admin() user_updated = self.get_admin()
role = Role.query.get(role.id) role = Role.query.get(role.id)
self.assertTrue(user_updated in role.members) self.assertTrue(user_updated in role.members)
...@@ -170,14 +167,14 @@ class TestRolemodViews(UffdTestCase): ...@@ -170,14 +167,14 @@ class TestRolemodViews(UffdTestCase):
db.session.add(Role(name='other_role', moderator_group=self.get_access_group())) db.session.add(Role(name='other_role', moderator_group=self.get_access_group()))
role = Role(name='test') role = Role(name='test')
db.session.add(role) db.session.add(role)
role.members.add(self.get_admin()) role.members.append(self.get_admin())
db.session.commit() db.session.commit()
user = self.get_admin() user = self.get_admin()
role = Role.query.get(role.id) role = Role.query.get(role.id)
self.assertTrue(user in role.members) self.assertTrue(user in role.members)
r = self.client.get(path=url_for('rolemod.delete_member', role_id=role.id, member_dn=user.dn), follow_redirects=True) r = self.client.get(path=url_for('rolemod.delete_member', role_id=role.id, member_id=user.id), follow_redirects=True)
dump('rolemod_delete_member_nomod', r) dump('rolemod_delete_member_nomod', r)
self.assertIn('Access denied'.encode(), r.data) self.assertEqual(r.status_code, 403)
user_updated = self.get_admin() user_updated = self.get_admin()
role = Role.query.get(role.id) role = Role.query.get(role.id)
self.assertTrue(user_updated in role.members) self.assertTrue(user_updated in role.members)
......
import datetime
import re
import time
from flask import url_for, request, session
from uffd.database import db
from uffd.models import PasswordToken, UserEmail, Role, RoleGroup, Service, ServiceUser, FeatureFlag, MFAMethod, RecoveryCodeMethod, TOTPMethod, WebauthnMethod
from uffd.models.mfa import _hotp
from tests.utils import dump, UffdTestCase, db_flush
class TestSelfservice(UffdTestCase):
def test_index(self):
self.login_as('user')
r = self.client.get(path=url_for('selfservice.index'))
dump('selfservice_index', r)
self.assertEqual(r.status_code, 200)
user = request.user
self.assertIn(user.displayname.encode(), r.data)
self.assertIn(user.loginname.encode(), r.data)
self.assertIn(user.primary_email.address.encode(), r.data)
def test_update_displayname(self):
self.login_as('user')
r = self.client.post(path=url_for('selfservice.update_profile'),
data={'displayname': 'New Display Name'},
follow_redirects=True)
dump('update_displayname', r)
self.assertEqual(r.status_code, 200)
user = self.get_user()
self.assertEqual(user.displayname, 'New Display Name')
def test_update_displayname_invalid(self):
self.login_as('user')
r = self.client.post(path=url_for('selfservice.update_profile'),
data={'displayname': ''},
follow_redirects=True)
dump('update_displayname_invalid', r)
self.assertEqual(r.status_code, 200)
user = self.get_user()
self.assertNotEqual(user.displayname, '')
def test_add_email(self):
self.login_as('user')
r = self.client.post(path=url_for('selfservice.add_email'),
data={'address': 'new@example.com'},
follow_redirects=True)
dump('selfservice_add_email', r)
self.assertEqual(r.status_code, 200)
self.assertIn('new@example.com', self.app.last_mail['To'])
m = re.search(r'/email/([0-9]+)/verify/(.*)', str(self.app.last_mail.get_content()))
email_id, secret = m.groups()
email = UserEmail.query.get(email_id)
self.assertEqual(email.user.id, request.user.id)
self.assertEqual(email.address, 'new@example.com')
self.assertFalse(email.verified)
self.assertFalse(email.verification_expired)
self.assertTrue(email.verification_secret.verify(secret))
def test_add_email_duplicate(self):
self.login_as('user')
r = self.client.post(path=url_for('selfservice.add_email'),
data={'address': 'test@example.com'},
follow_redirects=True)
dump('selfservice_add_email_duplicate', r)
self.assertFalse(hasattr(self.app, 'last_mail'))
self.assertEqual(len(self.get_user().all_emails), 1)
self.assertEqual(UserEmail.query.filter_by(user=None).all(), [])
def test_verify_email(self):
self.login_as('user')
email = UserEmail(user=self.get_user(), address='new@example.com')
secret = email.start_verification()
db.session.add(email)
db.session.commit()
email_id = email.id
r = self.client.get(path=url_for('selfservice.verify_email', email_id=email_id, secret=secret), follow_redirects=True)
dump('selfservice_verify_email', r)
self.assertEqual(r.status_code, 200)
email = UserEmail.query.get(email_id)
self.assertTrue(email.verified)
self.assertEqual(self.get_user().primary_email.address, 'test@example.com')
def test_verify_email_notfound(self):
self.login_as('user')
r = self.client.get(path=url_for('selfservice.verify_email', email_id=2342, secret='invalidsecret'), follow_redirects=True)
dump('selfservice_verify_email_notfound', r)
def test_verify_email_wrong_user(self):
self.login_as('user')
email = UserEmail(user=self.get_admin(), address='new@example.com')
secret = email.start_verification()
db.session.add(email)
db.session.commit()
r = self.client.get(path=url_for('selfservice.verify_email', email_id=email.id, secret=secret), follow_redirects=True)
dump('selfservice_verify_email_wrong_user', r)
self.assertFalse(email.verified)
def test_verify_email_wrong_secret(self):
self.login_as('user')
email = UserEmail(user=self.get_user(), address='new@example.com')
secret = email.start_verification()
db.session.add(email)
db.session.commit()
r = self.client.get(path=url_for('selfservice.verify_email', email_id=email.id, secret='invalidsecret'), follow_redirects=True)
dump('selfservice_verify_email_wrong_secret', r)
self.assertFalse(email.verified)
def test_verify_email_expired(self):
self.login_as('user')
email = UserEmail(user=self.get_user(), address='new@example.com')
secret = email.start_verification()
email.verification_expires = datetime.datetime.utcnow() - datetime.timedelta(days=1)
db.session.add(email)
db.session.commit()
r = self.client.get(path=url_for('selfservice.verify_email', email_id=email.id, secret=secret), follow_redirects=True)
dump('selfservice_verify_email_expired', r)
self.assertFalse(email.verified)
def test_verify_email_legacy(self):
self.login_as('user')
email = UserEmail(
user=self.get_user(),
address='new@example.com',
verification_legacy_id=1337,
_verification_secret='{PLAIN}ZgvsUs2bZjr9Whpy1la7Q0PHbhjmpXtNdH1mCmDbQP7',
verification_expires=datetime.datetime.utcnow()+datetime.timedelta(days=1)
)
db.session.add(email)
db.session.commit()
email_id = email.id
r = self.client.get(path=f'/self/token/mail_verification/1337/ZgvsUs2bZjr9Whpy1la7Q0PHbhjmpXtNdH1mCmDbQP7', follow_redirects=True)
dump('selfservice_verify_email_legacy', r)
self.assertEqual(r.status_code, 200)
email = UserEmail.query.get(email_id)
self.assertTrue(email.verified)
self.assertEqual(self.get_user().primary_email, email)
def test_verify_email_duplicate_strict_uniqueness(self):
FeatureFlag.unique_email_addresses.enable()
db.session.commit()
self.login_as('user')
email = UserEmail(user=self.get_user(), address='admin@example.com')
secret = email.start_verification()
db.session.add(email)
db.session.commit()
email_id = email.id
r = self.client.get(path=url_for('selfservice.verify_email', email_id=email.id, secret=secret), follow_redirects=True)
dump('selfservice_verify_email_duplicate_strict_uniqueness', r)
email = UserEmail.query.get(email_id)
self.assertFalse(email.verified)
def test_retry_email_verification(self):
self.login_as('user')
email = UserEmail(user=self.get_user(), address='new@example.com')
old_secret = email.start_verification()
db.session.add(email)
db.session.commit()
r = self.client.get(path=url_for('selfservice.retry_email_verification', email_id=email.id), follow_redirects=True)
dump('selfservice_retry_email_verification', r)
self.assertEqual(r.status_code, 200)
self.assertIn('new@example.com', self.app.last_mail['To'])
m = re.search(r'/email/([0-9]+)/verify/(.*)', str(self.app.last_mail.get_content()))
email_id, secret = m.groups()
email = UserEmail.query.get(email_id)
self.assertEqual(email.user.id, request.user.id)
self.assertEqual(email.address, 'new@example.com')
self.assertFalse(email.verified)
self.assertFalse(email.verification_expired)
self.assertTrue(email.verification_secret.verify(secret))
self.assertFalse(email.verification_secret.verify(old_secret))
def test_delete_email(self):
self.login_as('user')
email = UserEmail(user=self.get_user(), address='new@example.com', verified=True)
db.session.add(email)
self.get_user().recovery_email = email
db.session.commit()
r = self.client.post(path=url_for('selfservice.delete_email', email_id=email.id), follow_redirects=True)
dump('selfservice_delete_email', r)
self.assertEqual(r.status_code, 200)
self.assertIsNone(UserEmail.query.filter_by(address='new@example.com').first())
self.assertIsNone(self.get_user().recovery_email)
self.assertEqual(self.get_user().primary_email.address, 'test@example.com')
def test_delete_email_invalid(self):
self.login_as('user')
r = self.client.post(path=url_for('selfservice.delete_email', email_id=2324), follow_redirects=True)
self.assertEqual(r.status_code, 404)
def test_delete_email_primary(self):
self.login_as('user')
r = self.client.post(path=url_for('selfservice.delete_email', email_id=request.user.primary_email.id), follow_redirects=True)
dump('selfservice_delete_email_primary', r)
self.assertEqual(self.get_user().primary_email.address, 'test@example.com')
def test_update_email_preferences(self):
self.login_as('user')
user_id = self.get_user().id
email = UserEmail(user=self.get_user(), address='new@example.com', verified=True)
db.session.add(email)
service = Service(name='service', enable_email_preferences=True, limit_access=False)
db.session.add(service)
db.session.commit()
email_id = email.id
service_id = service.id
old_email_id = self.get_user().primary_email.id
r = self.client.post(path=url_for('selfservice.update_email_preferences'),
data={'primary_email': str(email_id), 'recovery_email': 'primary'},
follow_redirects=True)
dump('selfservice_update_email_preferences', r)
self.assertEqual(r.status_code, 200)
self.assertEqual(self.get_user().primary_email.id, email.id)
self.assertIsNone(self.get_user().recovery_email)
r = self.client.post(path=url_for('selfservice.update_email_preferences'),
data={'primary_email': str(old_email_id), 'recovery_email': str(email_id)},
follow_redirects=True)
self.assertEqual(r.status_code, 200)
self.assertEqual(self.get_user().primary_email.id, old_email_id)
self.assertEqual(self.get_user().recovery_email.id, email_id)
r = self.client.post(path=url_for('selfservice.update_email_preferences'),
data={'primary_email': str(old_email_id), 'recovery_email': 'primary', f'service_{service_id}_email': 'primary'},
follow_redirects=True)
self.assertEqual(r.status_code, 200)
self.assertIsNone(ServiceUser.query.get((service_id, user_id)).service_email)
r = self.client.post(path=url_for('selfservice.update_email_preferences'),
data={'primary_email': str(old_email_id), 'recovery_email': 'primary', f'service_{service_id}_email': str(email_id)},
follow_redirects=True)
self.assertEqual(r.status_code, 200)
self.assertEqual(ServiceUser.query.get((service_id, user_id)).service_email.id, email_id)
def test_update_email_preferences_unverified(self):
self.login_as('user')
user_id = self.get_user().id
email = UserEmail(user=self.get_user(), address='new@example.com')
db.session.add(email)
service = Service(name='service', enable_email_preferences=True, limit_access=False)
db.session.add(service)
db.session.commit()
email_id = email.id
service_id = service.id
old_email_id = self.get_user().primary_email.id
with self.assertRaises(Exception):
r = self.client.post(path=url_for('selfservice.update_email_preferences'),
data={'primary_email': str(email_id), 'recovery_email': 'primary'},
follow_redirects=True)
with self.app.test_request_context():
self.assertEqual(self.get_user().primary_email.address, 'test@example.com')
with self.assertRaises(Exception):
r = self.client.post(path=url_for('selfservice.update_email_preferences'),
data={'primary_email': str(old_email_id), 'recovery_email': str(email_id)},
follow_redirects=True)
with self.app.test_request_context():
self.assertIsNone(self.get_user().recovery_email)
with self.assertRaises(Exception):
r = self.client.post(path=url_for('selfservice.update_email_preferences'),
data={'primary_email': str(old_email_id), 'recovery_email': 'primary', f'service_{service_id}_email': str(email_id)},
follow_redirects=True)
with self.app.test_request_context():
self.assertIsNone(ServiceUser.query.get((service_id, user_id)).service_email)
def test_update_email_preferences_invalid(self):
self.login_as('user')
user_id = self.get_user().id
email = UserEmail(user=self.get_user(), address='new@example.com', verified=True)
db.session.add(email)
service = Service(name='service', enable_email_preferences=True, limit_access=False)
db.session.add(service)
db.session.commit()
with self.assertRaises(Exception):
r = self.client.post(path=url_for('selfservice.update_email_preferences'),
data={'primary_email': str(email.id), 'recovery_email': '2342'},
follow_redirects=True)
with self.assertRaises(Exception):
r = self.client.post(path=url_for('selfservice.update_email_preferences'),
data={'primary_email': str(email.id), 'recovery_email': 'primary', f'service_{service_id}_email': '2342'},
follow_redirects=True)
with self.assertRaises(Exception):
r = self.client.post(path=url_for('selfservice.update_email_preferences'),
data={'primary_email': 'primary', 'recovery_email': 'primary'},
follow_redirects=True)
with self.assertRaises(Exception):
r = self.client.post(path=url_for('selfservice.update_email_preferences'),
data={'primary_email': '2342', 'recovery_email': 'primary'},
follow_redirects=True)
def test_change_password(self):
self.login_as('user')
r = self.client.post(path=url_for('selfservice.change_password'),
data={'password1': 'newpassword', 'password2': 'newpassword'},
follow_redirects=True)
dump('change_password', r)
self.assertEqual(r.status_code, 200)
self.assertTrue(self.get_user().password.verify('newpassword'))
def test_change_password_invalid(self):
self.login_as('user')
r = self.client.post(path=url_for('selfservice.change_password'),
data={'password1': 'shortpw', 'password2': 'shortpw'},
follow_redirects=True)
dump('change_password_invalid', r)
self.assertEqual(r.status_code, 200)
user = self.get_user()
self.assertFalse(user.password.verify('shortpw'))
self.assertTrue(user.password.verify('userpassword'))
# Regression test for #100 (login not possible if password contains character disallowed by SASLprep)
def test_change_password_samlprep_invalid(self):
self.login_as('user')
r = self.client.post(path=url_for('selfservice.change_password'),
data={'password1': 'shortpw\n', 'password2': 'shortpw\n'},
follow_redirects=True)
dump('change_password_samlprep_invalid', r)
self.assertEqual(r.status_code, 200)
user = self.get_user()
self.assertFalse(user.password.verify('shortpw\n'))
self.assertTrue(user.password.verify('userpassword'))
def test_change_password_mismatch(self):
self.login_as('user')
r = self.client.post(path=url_for('selfservice.change_password'),
data={'password1': 'newpassword1', 'password2': 'newpassword2'},
follow_redirects=True)
dump('change_password_mismatch', r)
self.assertEqual(r.status_code, 200)
user = self.get_user()
self.assertFalse(user.password.verify('newpassword1'))
self.assertFalse(user.password.verify('newpassword2'))
self.assertTrue(user.password.verify('userpassword'))
def test_leave_role(self):
baserole = Role(name='baserole', is_default=True)
db.session.add(baserole)
baserole.groups[self.get_access_group()] = RoleGroup()
role1 = Role(name='testrole1')
role2 = Role(name='testrole2')
db.session.add(role1)
db.session.add(role2)
self.get_user().roles = [role1, role2]
db.session.commit()
roleid = role1.id
self.login_as('user')
r = self.client.post(path=url_for('selfservice.leave_role', roleid=roleid), follow_redirects=True)
dump('leave_role', r)
self.assertEqual(r.status_code, 200)
_user = self.get_user()
self.assertEqual(len(_user.roles), 1)
self.assertEqual(list(_user.roles)[0].name, 'testrole2')
def test_forgot_password(self):
user = self.get_user()
r = self.client.get(path=url_for('selfservice.forgot_password'))
dump('forgot_password', r)
self.assertEqual(r.status_code, 200)
user = self.get_user()
r = self.client.post(path=url_for('selfservice.forgot_password'),
data={'loginname': user.loginname, 'mail': user.primary_email.address}, follow_redirects=True)
dump('forgot_password_submit', r)
self.assertEqual(r.status_code, 200)
token = PasswordToken.query.filter(PasswordToken.user == user).first()
self.assertIsNotNone(token)
self.assertIn(token.token, str(self.app.last_mail.get_content()))
def test_forgot_password_wrong_user(self):
user = self.get_user()
r = self.client.get(path=url_for('selfservice.forgot_password'))
self.assertEqual(r.status_code, 200)
user = self.get_user()
r = self.client.post(path=url_for('selfservice.forgot_password'),
data={'loginname': 'not_a_user', 'mail': user.primary_email.address}, follow_redirects=True)
dump('forgot_password_submit_wrong_user', r)
self.assertEqual(r.status_code, 200)
self.assertFalse(hasattr(self.app, 'last_mail'))
self.assertEqual(len(PasswordToken.query.all()), 0)
def test_forgot_password_wrong_email(self):
user = self.get_user()
r = self.client.get(path=url_for('selfservice.forgot_password'), follow_redirects=True)
self.assertEqual(r.status_code, 200)
r = self.client.post(path=url_for('selfservice.forgot_password'),
data={'loginname': user.loginname, 'mail': 'not_an_email@example.com'}, follow_redirects=True)
dump('forgot_password_submit_wrong_email', r)
self.assertEqual(r.status_code, 200)
self.assertFalse(hasattr(self.app, 'last_mail'))
self.assertEqual(len(PasswordToken.query.all()), 0)
# Regression test for #31
def test_forgot_password_invalid_user(self):
r = self.client.post(path=url_for('selfservice.forgot_password'),
data={'loginname': '=', 'mail': 'test@example.com'}, follow_redirects=True)
dump('forgot_password_submit_invalid_user', r)
self.assertEqual(r.status_code, 200)
self.assertFalse(hasattr(self.app, 'last_mail'))
self.assertEqual(len(PasswordToken.query.all()), 0)
def test_forgot_password_wrong_user(self):
user = self.get_user()
r = self.client.get(path=url_for('selfservice.forgot_password'))
self.assertEqual(r.status_code, 200)
user = self.get_user()
user.is_deactivated = True
db.session.commit()
r = self.client.post(path=url_for('selfservice.forgot_password'),
data={'loginname': user.loginname, 'mail': user.primary_email.address}, follow_redirects=True)
self.assertEqual(r.status_code, 200)
self.assertFalse(hasattr(self.app, 'last_mail'))
self.assertEqual(len(PasswordToken.query.all()), 0)
def test_token_password(self):
user = self.get_user()
token = PasswordToken(user=user)
db.session.add(token)
db.session.commit()
self.assertFalse(token.expired)
r = self.client.get(path=url_for('selfservice.token_password', token_id=token.id, token=token.token), follow_redirects=True)
dump('token_password', r)
self.assertEqual(r.status_code, 200)
r = self.client.post(path=url_for('selfservice.token_password', token_id=token.id, token=token.token),
data={'password1': 'newpassword', 'password2': 'newpassword'}, follow_redirects=True)
dump('token_password_submit', r)
self.assertEqual(r.status_code, 200)
self.assertTrue(self.get_user().password.verify('newpassword'))
def test_token_password_emptydb(self):
user = self.get_user()
r = self.client.get(path=url_for('selfservice.token_password', token_id=1, token='A'*128), follow_redirects=True)
dump('token_password_emptydb', r)
self.assertEqual(r.status_code, 200)
self.assertIn(b'Link invalid or expired', r.data)
r = self.client.post(path=url_for('selfservice.token_password', token_id=1, token='A'*128),
data={'password1': 'newpassword', 'password2': 'newpassword'}, follow_redirects=True)
dump('token_password_emptydb_submit', r)
self.assertEqual(r.status_code, 200)
self.assertIn(b'Link invalid or expired', r.data)
self.assertTrue(self.get_user().password.verify('userpassword'))
def test_token_password_invalid(self):
user = self.get_user()
token = PasswordToken(user=user)
db.session.add(token)
db.session.commit()
r = self.client.get(path=url_for('selfservice.token_password', token_id=token.id, token='A'*128), follow_redirects=True)
dump('token_password_invalid', r)
self.assertEqual(r.status_code, 200)
self.assertIn(b'Link invalid or expired', r.data)
r = self.client.post(path=url_for('selfservice.token_password', token_id=token.id, token='A'*128),
data={'password1': 'newpassword', 'password2': 'newpassword'}, follow_redirects=True)
dump('token_password_invalid_submit', r)
self.assertEqual(r.status_code, 200)
self.assertIn(b'Link invalid or expired', r.data)
self.assertTrue(self.get_user().password.verify('userpassword'))
def test_token_password_expired(self):
user = self.get_user()
token = PasswordToken(user=user, created=(datetime.datetime.utcnow() - datetime.timedelta(days=10)))
db.session.add(token)
db.session.commit()
self.assertTrue(token.expired)
r = self.client.get(path=url_for('selfservice.token_password', token_id=token.id, token=token.token), follow_redirects=True)
dump('token_password_invalid_expired', r)
self.assertEqual(r.status_code, 200)
self.assertIn(b'Link invalid or expired', r.data)
r = self.client.post(path=url_for('selfservice.token_password', token_id=token.id, token=token.token),
data={'password1': 'newpassword', 'password2': 'newpassword'}, follow_redirects=True)
dump('token_password_invalid_expired_submit', r)
self.assertEqual(r.status_code, 200)
self.assertIn(b'Link invalid or expired', r.data)
self.assertTrue(self.get_user().password.verify('userpassword'))
def test_token_password_different_passwords(self):
user = self.get_user()
token = PasswordToken(user=user)
db.session.add(token)
db.session.commit()
r = self.client.get(path=url_for('selfservice.token_password', token_id=token.id, token=token.token), follow_redirects=True)
self.assertEqual(r.status_code, 200)
r = self.client.post(path=url_for('selfservice.token_password', token_id=token.id, token=token.token),
data={'password1': 'newpassword', 'password2': 'differentpassword'}, follow_redirects=True)
dump('token_password_different_passwords_submit', r)
self.assertEqual(r.status_code, 200)
self.assertTrue(self.get_user().password.verify('userpassword'))
def get_fido2_test_cred(self):
try:
from uffd.fido2_compat import AttestedCredentialData
except ImportError:
self.skipTest('fido2 could not be imported')
# Example public key from webauthn spec 6.5.1.1
return AttestedCredentialData(bytes.fromhex('00000000000000000000000000000000'+'0040'+'053cbcc9d37a61d3bac87cdcc77ee326256def08ab15775d3a720332e4101d14fae95aeee3bc9698781812e143c0597dc6e180595683d501891e9dd030454c0a'+'A501020326200121582065eda5a12577c2bae829437fe338701a10aaa375e1bb5b5de108de439c08551d2258201e52ed75701163f7f9e40ddf9f341b3dc9ba860af7e0ca7ca7e9eecd0084d19c'))
class TestMfaViews(UffdTestCase):
def setUp(self):
super().setUp()
db.session.add(RecoveryCodeMethod(user=self.get_admin()))
db.session.add(TOTPMethod(user=self.get_admin(), name='Admin Phone'))
# We don't want to skip all tests only because fido2 is not installed!
#db.session.add(WebauthnMethod(user=get_testadmin(), cred=get_fido2_test_cred(self), name='Admin FIDO2 dongle'))
db.session.commit()
def add_recovery_codes(self, count=10):
user = self.get_user()
for _ in range(count):
db.session.add(RecoveryCodeMethod(user=user))
db.session.commit()
def add_totp(self):
db.session.add(TOTPMethod(user=self.get_user(), name='My phone'))
db.session.commit()
def add_webauthn(self):
db.session.add(WebauthnMethod(user=self.get_user(), cred=get_fido2_test_cred(self), name='My FIDO2 dongle'))
db.session.commit()
def test_setup_disabled(self):
self.login_as('user')
r = self.client.get(path=url_for('selfservice.setup_mfa'), follow_redirects=True)
dump('mfa_setup_disabled', r)
self.assertEqual(r.status_code, 200)
def test_setup_recovery_codes(self):
self.login_as('user')
self.add_recovery_codes()
r = self.client.get(path=url_for('selfservice.setup_mfa'), follow_redirects=True)
dump('mfa_setup_only_recovery_codes', r)
self.assertEqual(r.status_code, 200)
def test_setup_enabled(self):
self.login_as('user')
self.add_recovery_codes()
self.add_totp()
self.add_webauthn()
r = self.client.get(path=url_for('selfservice.setup_mfa'), follow_redirects=True)
dump('mfa_setup_enabled', r)
self.assertEqual(r.status_code, 200)
def test_setup_few_recovery_codes(self):
self.login_as('user')
self.add_totp()
self.add_recovery_codes(1)
r = self.client.get(path=url_for('selfservice.setup_mfa'), follow_redirects=True)
dump('mfa_setup_few_recovery_codes', r)
self.assertEqual(r.status_code, 200)
def test_setup_no_recovery_codes(self):
self.login_as('user')
self.add_totp()
r = self.client.get(path=url_for('selfservice.setup_mfa'), follow_redirects=True)
dump('mfa_setup_no_recovery_codes', r)
self.assertEqual(r.status_code, 200)
def test_disable(self):
baserole = Role(name='baserole', is_default=True)
db.session.add(baserole)
baserole.groups[self.get_access_group()] = RoleGroup()
db.session.commit()
self.login_as('user')
self.add_recovery_codes()
self.add_totp()
admin_methods = len(MFAMethod.query.filter_by(user=self.get_admin()).all())
r = self.client.get(path=url_for('selfservice.disable_mfa'), follow_redirects=True)
dump('mfa_disable', r)
self.assertEqual(r.status_code, 200)
r = self.client.post(path=url_for('selfservice.disable_mfa_confirm'), follow_redirects=True)
dump('mfa_disable_submit', r)
self.assertEqual(r.status_code, 200)
self.assertEqual(len(MFAMethod.query.filter_by(user=request.user).all()), 0)
self.assertEqual(len(MFAMethod.query.filter_by(user=self.get_admin()).all()), admin_methods)
def test_disable_recovery_only(self):
baserole = Role(name='baserole', is_default=True)
db.session.add(baserole)
baserole.groups[self.get_access_group()] = RoleGroup()
db.session.commit()
self.login_as('user')
self.add_recovery_codes()
admin_methods = len(MFAMethod.query.filter_by(user=self.get_admin()).all())
self.assertNotEqual(len(MFAMethod.query.filter_by(user=request.user).all()), 0)
r = self.client.get(path=url_for('selfservice.disable_mfa'), follow_redirects=True)
dump('mfa_disable_recovery_only', r)
self.assertEqual(r.status_code, 200)
r = self.client.post(path=url_for('selfservice.disable_mfa_confirm'), follow_redirects=True)
dump('mfa_disable_recovery_only_submit', r)
self.assertEqual(r.status_code, 200)
self.assertEqual(len(MFAMethod.query.filter_by(user=request.user).all()), 0)
self.assertEqual(len(MFAMethod.query.filter_by(user=self.get_admin()).all()), admin_methods)
def test_setup_recovery(self):
self.login_as('user')
self.assertEqual(len(RecoveryCodeMethod.query.filter_by(user=request.user).all()), 0)
r = self.client.post(path=url_for('selfservice.setup_mfa_recovery'), follow_redirects=True)
dump('mfa_setup_recovery', r)
self.assertEqual(r.status_code, 200)
methods = RecoveryCodeMethod.query.filter_by(user=request.user).all()
self.assertNotEqual(len(methods), 0)
r = self.client.post(path=url_for('selfservice.setup_mfa_recovery'), follow_redirects=True)
dump('mfa_setup_recovery_reset', r)
self.assertEqual(r.status_code, 200)
self.assertEqual(len(RecoveryCodeMethod.query.filter_by(id=methods[0].id).all()), 0)
self.assertNotEqual(len(methods), 0)
def test_setup_totp(self):
self.login_as('user')
self.add_recovery_codes()
r = self.client.get(path=url_for('selfservice.setup_mfa_totp', name='My TOTP Authenticator'), follow_redirects=True)
dump('mfa_setup_totp', r)
self.assertEqual(r.status_code, 200)
self.assertNotEqual(len(session.get('mfa_totp_key', '')), 0)
def test_setup_totp_without_recovery(self):
self.login_as('user')
r = self.client.get(path=url_for('selfservice.setup_mfa_totp', name='My TOTP Authenticator'), follow_redirects=True)
dump('mfa_setup_totp_without_recovery', r)
self.assertEqual(r.status_code, 200)
def test_setup_totp_finish(self):
baserole = Role(name='baserole', is_default=True)
db.session.add(baserole)
baserole.groups[self.get_access_group()] = RoleGroup()
db.session.commit()
self.login_as('user')
self.add_recovery_codes()
self.assertEqual(len(TOTPMethod.query.filter_by(user=request.user).all()), 0)
r = self.client.get(path=url_for('selfservice.setup_mfa_totp', name='My TOTP Authenticator'), follow_redirects=True)
method = TOTPMethod(request.user, key=session.get('mfa_totp_key', ''))
code = _hotp(int(time.time()/30), method.raw_key)
r = self.client.post(path=url_for('selfservice.setup_mfa_totp_finish', name='My TOTP Authenticator'), data={'code': code}, follow_redirects=True)
dump('mfa_setup_totp_finish', r)
self.assertEqual(r.status_code, 200)
self.assertEqual(len(TOTPMethod.query.filter_by(user=request.user).all()), 1)
def test_setup_totp_finish_without_recovery(self):
self.login_as('user')
self.assertEqual(len(TOTPMethod.query.filter_by(user=request.user).all()), 0)
r = self.client.get(path=url_for('selfservice.setup_mfa_totp', name='My TOTP Authenticator'), follow_redirects=True)
method = TOTPMethod(request.user, key=session.get('mfa_totp_key', ''))
code = _hotp(int(time.time()/30), method.raw_key)
r = self.client.post(path=url_for('selfservice.setup_mfa_totp_finish', name='My TOTP Authenticator'), data={'code': code}, follow_redirects=True)
dump('mfa_setup_totp_finish_without_recovery', r)
self.assertEqual(r.status_code, 200)
self.assertEqual(len(TOTPMethod.query.filter_by(user=request.user).all()), 0)
def test_setup_totp_finish_wrong_code(self):
self.login_as('user')
self.add_recovery_codes()
self.assertEqual(len(TOTPMethod.query.filter_by(user=request.user).all()), 0)
r = self.client.get(path=url_for('selfservice.setup_mfa_totp', name='My TOTP Authenticator'), follow_redirects=True)
method = TOTPMethod(request.user, key=session.get('mfa_totp_key', ''))
code = _hotp(int(time.time()/30), method.raw_key)
code = str(int(code[0])+1)[-1] + code[1:]
r = self.client.post(path=url_for('selfservice.setup_mfa_totp_finish', name='My TOTP Authenticator'), data={'code': code}, follow_redirects=True)
dump('mfa_setup_totp_finish_wrong_code', r)
self.assertEqual(r.status_code, 200)
db_flush()
self.assertEqual(len(TOTPMethod.query.filter_by(user=request.user).all()), 0)
def test_setup_totp_finish_empty_code(self):
self.login_as('user')
self.add_recovery_codes()
self.assertEqual(len(TOTPMethod.query.filter_by(user=request.user).all()), 0)
r = self.client.get(path=url_for('selfservice.setup_mfa_totp', name='My TOTP Authenticator'), follow_redirects=True)
r = self.client.post(path=url_for('selfservice.setup_mfa_totp_finish', name='My TOTP Authenticator'), data={'code': ''}, follow_redirects=True)
dump('mfa_setup_totp_finish_empty_code', r)
self.assertEqual(r.status_code, 200)
db_flush()
self.assertEqual(len(TOTPMethod.query.filter_by(user=request.user).all()), 0)
def test_delete_totp(self):
baserole = Role(name='baserole', is_default=True)
db.session.add(baserole)
baserole.groups[self.get_access_group()] = RoleGroup()
db.session.commit()
self.login_as('user')
self.add_recovery_codes()
self.add_totp()
method = TOTPMethod(request.user, name='test')
db.session.add(method)
db.session.commit()
self.assertEqual(len(TOTPMethod.query.filter_by(user=request.user).all()), 2)
r = self.client.get(path=url_for('selfservice.delete_mfa_totp', id=method.id), follow_redirects=True)
dump('mfa_delete_totp', r)
self.assertEqual(r.status_code, 200)
self.assertEqual(len(TOTPMethod.query.filter_by(id=method.id).all()), 0)
self.assertEqual(len(TOTPMethod.query.filter_by(user=request.user).all()), 1)
# TODO: webauthn setup tests
from flask import url_for
from uffd.database import db
from uffd.models import Service, ServiceUser, OAuth2Client, APIClient, RemailerMode
from tests.utils import dump, UffdTestCase
class TestServices(UffdTestCase):
def setUpApp(self):
self.app.config['SERVICES'] = [
{
'title': 'Service Title',
'subtitle': 'Service Subtitle',
'description': 'Short description of the service as plain text',
'url': 'https://example.com/',
'logo_url': '/static/fairy-dust-color.png',
'required_group': 'users',
'permission_levels': [
{'name': 'Moderator', 'required_group': 'moderators'},
{'name': 'Admin', 'required_group': 'uffd_admin'},
],
'confidential': True,
'groups': [
{'name': 'Group "crew_crew"', 'required_group': 'users'},
{'name': 'Group "crew_logistik"', 'required_group': 'uffd_admin'},
],
'infos': [
{'title': 'Documentation', 'html': '<p>Some information about the service as html</p>', 'required_group': 'users'},
],
'links': [
{'title': 'Link to an external site', 'url': '#', 'required_group': 'users'},
],
},
{
'title': 'Minimal Service Title',
}
]
self.app.config['SERVICES_PUBLIC'] = True
def test_overview(self):
r = self.client.get(path=url_for('service.overview'))
dump('service_overview_guest', r)
self.assertEqual(r.status_code, 200)
self.assertNotIn(b'https://example.com/', r.data)
self.login_as('user')
r = self.client.get(path=url_for('service.overview'))
dump('service_overview_user', r)
self.assertEqual(r.status_code, 200)
self.assertIn(b'https://example.com/', r.data)
def test_overview_disabled(self):
self.app.config['SERVICES'] = []
# Should return login page
r = self.client.get(path=url_for('service.overview'), follow_redirects=True)
dump('service_overview_disabled_guest', r)
self.assertEqual(r.status_code, 200)
self.assertIn(b'name="password"', r.data)
self.login_as('user')
# Should return access denied page
r = self.client.get(path=url_for('service.overview'), follow_redirects=True)
dump('service_overview_disabled_user', r)
self.assertEqual(r.status_code, 403)
self.login_as('admin')
# Should return (empty) overview page
r = self.client.get(path=url_for('service.overview'), follow_redirects=True)
dump('service_overview_disabled_admin', r)
self.assertEqual(r.status_code, 200)
def test_overview_nonpublic(self):
self.app.config['SERVICES_PUBLIC'] = False
# Should return login page
r = self.client.get(path=url_for('service.overview'), follow_redirects=True)
dump('service_overview_nonpublic_guest', r)
self.assertEqual(r.status_code, 200)
self.assertIn(b'name="password"', r.data)
self.login_as('user')
# Should return overview page
r = self.client.get(path=url_for('service.overview'), follow_redirects=True)
dump('service_overview_nonpublic_user', r)
self.assertEqual(r.status_code, 200)
self.login_as('admin')
# Should return overview page
r = self.client.get(path=url_for('service.overview'), follow_redirects=True)
dump('service_overview_nonpublic_admin', r)
self.assertEqual(r.status_code, 200)
def test_overview_public(self):
# Should return overview page
r = self.client.get(path=url_for('service.overview'), follow_redirects=True)
dump('service_overview_public_guest', r)
self.assertEqual(r.status_code, 200)
self.login_as('user')
# Should return overview page
r = self.client.get(path=url_for('service.overview'), follow_redirects=True)
dump('service_overview_public_user', r)
self.assertEqual(r.status_code, 200)
self.login_as('admin')
# Should return overview page
r = self.client.get(path=url_for('service.overview'), follow_redirects=True)
dump('service_overview_public_admin', r)
self.assertEqual(r.status_code, 200)
class TestServiceAdminViews(UffdTestCase):
def setUpDB(self):
db.session.add(Service(
name='test1',
oauth2_clients=[OAuth2Client(client_id='test1_oauth2_client1', client_secret='test'), OAuth2Client(client_id='test1_oauth2_client2', client_secret='test')],
api_clients=[APIClient(auth_username='test1_api_client1', auth_password='test'), APIClient(auth_username='test1_api_client2', auth_password='test')],
))
db.session.add(Service(name='test2'))
db.session.add(Service(name='test3'))
db.session.commit()
self.service_id = Service.query.filter_by(name='test1').one().id
def test_index(self):
self.login_as('admin')
r = self.client.get(path=url_for('service.index'), follow_redirects=True)
dump('service_index', r)
self.assertEqual(r.status_code, 200)
def test_show(self):
self.login_as('admin')
r = self.client.get(path=url_for('service.show', id=self.service_id), follow_redirects=True)
dump('service_show', r)
self.assertEqual(r.status_code, 200)
def test_new(self):
self.login_as('admin')
r = self.client.get(path=url_for('service.show'), follow_redirects=True)
dump('service_new', r)
self.assertEqual(r.status_code, 200)
def test_new_submit(self):
self.login_as('admin')
r = self.client.post(
path=url_for('service.edit_submit'),
follow_redirects=True,
data={
'name': 'new-service',
'access-group': '',
'remailer-mode': 'DISABLED',
'remailer-overwrite-mode': 'ENABLED_V2',
'remailer-overwrite-users': '',
},
)
dump('service_new_submit', r)
self.assertEqual(r.status_code, 200)
service = Service.query.filter_by(name='new-service').one_or_none()
self.assertIsNotNone(service)
self.assertEqual(service.limit_access, True)
self.assertEqual(service.access_group, None)
self.assertEqual(service.remailer_mode, RemailerMode.DISABLED)
self.assertEqual(service.enable_email_preferences, False)
def test_edit(self):
self.login_as('admin')
r = self.client.post(
path=url_for('service.edit_submit', id=self.service_id),
follow_redirects=True,
data={
'name': 'new-name',
'access-group': '',
'remailer-mode': 'DISABLED',
'remailer-overwrite-mode': 'ENABLED_V2',
'remailer-overwrite-users': '',
},
)
dump('service_edit_submit', r)
self.assertEqual(r.status_code, 200)
service = Service.query.get(self.service_id)
self.assertEqual(service.name, 'new-name')
self.assertEqual(service.limit_access, True)
self.assertEqual(service.access_group, None)
self.assertEqual(service.remailer_mode, RemailerMode.DISABLED)
self.assertEqual(service.enable_email_preferences, False)
self.assertEqual(service.hide_deactivated_users, False)
def test_edit_access_all(self):
self.login_as('admin')
r = self.client.post(
path=url_for('service.edit_submit', id=self.service_id),
follow_redirects=True,
data={
'name': 'test1',
'access-group': 'all',
'remailer-mode': 'DISABLED',
'remailer-overwrite-mode': 'ENABLED_V2',
'remailer-overwrite-users': '',
},
)
self.assertEqual(r.status_code, 200)
service = Service.query.get(self.service_id)
self.assertEqual(service.limit_access, False)
self.assertEqual(service.access_group, None)
def test_edit_access_group(self):
self.login_as('admin')
r = self.client.post(
path=url_for('service.edit_submit', id=self.service_id),
follow_redirects=True,
data={
'name': 'test1',
'access-group': str(self.get_users_group().id),
'remailer-mode': 'DISABLED',
'remailer-overwrite-mode': 'ENABLED_V2',
'remailer-overwrite-users': '',
},
)
self.assertEqual(r.status_code, 200)
service = Service.query.get(self.service_id)
self.assertEqual(service.limit_access, True)
self.assertEqual(service.access_group, self.get_users_group())
def test_edit_hide_deactivated_users(self):
self.login_as('admin')
r = self.client.post(
path=url_for('service.edit_submit', id=self.service_id),
follow_redirects=True,
data={
'name': 'test1',
'access-group': '',
'remailer-mode': 'DISABLED',
'remailer-overwrite-mode': 'ENABLED_V2',
'remailer-overwrite-users': '',
'hide_deactivated_users': '1',
},
)
self.assertEqual(r.status_code, 200)
service = Service.query.get(self.service_id)
self.assertEqual(service.hide_deactivated_users, True)
def test_edit_email_preferences(self):
self.login_as('admin')
r = self.client.post(
path=url_for('service.edit_submit', id=self.service_id),
follow_redirects=True,
data={
'name': 'test1',
'access-group': '',
'remailer-mode': 'DISABLED',
'remailer-overwrite-mode': 'ENABLED_V2',
'remailer-overwrite-users': '',
'enable_email_preferences': '1',
},
)
self.assertEqual(r.status_code, 200)
service = Service.query.get(self.service_id)
self.assertEqual(service.enable_email_preferences, True)
def test_edit_remailer_mode(self):
self.login_as('admin')
r = self.client.post(
path=url_for('service.edit_submit', id=self.service_id),
follow_redirects=True,
data={
'name': 'test1',
'access-group': '',
'remailer-mode': 'ENABLED_V2',
'remailer-overwrite-mode': 'ENABLED_V2',
'remailer-overwrite-users': '',
},
)
self.assertEqual(r.status_code, 200)
service = Service.query.get(self.service_id)
self.assertEqual(service.remailer_mode, RemailerMode.ENABLED_V2)
def test_edit_remailer_overwrite_enable(self):
self.login_as('admin')
r = self.client.post(
path=url_for('service.edit_submit', id=self.service_id),
follow_redirects=True,
data={
'name': 'test1',
'access-group': '',
'remailer-mode': 'DISABLED',
'remailer-overwrite-mode': 'ENABLED_V2',
'remailer-overwrite-users': 'testuser, testadmin',
},
)
self.assertEqual(r.status_code, 200)
service_user1 = ServiceUser.query.get((self.service_id, self.get_user().id))
service_user2 = ServiceUser.query.get((self.service_id, self.get_admin().id))
self.assertEqual(service_user1.remailer_overwrite_mode, RemailerMode.ENABLED_V2)
self.assertEqual(service_user2.remailer_overwrite_mode, RemailerMode.ENABLED_V2)
self.assertEqual(
set(ServiceUser.query.filter(
ServiceUser.service_id == self.service_id,
ServiceUser.remailer_overwrite_mode != None
).all()),
{service_user1, service_user2}
)
def test_edit_remailer_overwrite_change(self):
service_user = ServiceUser.query.get((self.service_id, self.get_user().id))
service_user.remailer_overwrite_mode = RemailerMode.ENABLED_V2
db.session.commit()
self.login_as('admin')
r = self.client.post(
path=url_for('service.edit_submit', id=self.service_id),
follow_redirects=True,
data={
'name': 'test1',
'access-group': '',
'remailer-mode': 'DISABLED',
'remailer-overwrite-mode': 'ENABLED_V1',
'remailer-overwrite-users': ', testadmin',
},
)
self.assertEqual(r.status_code, 200)
service_user = ServiceUser.query.get((self.service_id, self.get_admin().id))
self.assertEqual(service_user.remailer_overwrite_mode, RemailerMode.ENABLED_V1)
self.assertEqual(
ServiceUser.query.filter(
ServiceUser.service_id == self.service_id,
ServiceUser.remailer_overwrite_mode != None
).all(),
[service_user]
)
def test_edit_remailer_overwrite_disable(self):
service_user = ServiceUser.query.get((self.service_id, self.get_user().id))
service_user.remailer_overwrite_mode = RemailerMode.ENABLED_V2
db.session.commit()
self.login_as('admin')
r = self.client.post(
path=url_for('service.edit_submit', id=self.service_id),
follow_redirects=True,
data={
'name': 'test1',
'access-group': '',
'remailer-mode': 'DISABLED',
'remailer-overwrite-mode': 'ENABLED_V2',
'remailer-overwrite-users': '',
},
)
self.assertEqual(r.status_code, 200)
self.assertEqual(
ServiceUser.query.filter(
ServiceUser.service_id == self.service_id,
ServiceUser.remailer_overwrite_mode != None
).all(),
[]
)
def test_delete(self):
self.login_as('admin')
r = self.client.get(path=url_for('service.delete', id=self.service_id), follow_redirects=True)
dump('service_delete', r)
self.assertEqual(r.status_code, 200)
self.assertIsNone(Service.query.get(self.service_id))
...@@ -3,15 +3,13 @@ import unittest ...@@ -3,15 +3,13 @@ import unittest
from flask import url_for, request from flask import url_for, request
# These imports are required, because otherwise we get circular imports?! from uffd.database import db
from uffd import ldap, user from uffd.password_hash import PlaintextPasswordHash
from uffd.models import DeviceLoginConfirmation, Service, OAuth2Client, OAuth2DeviceLoginInitiation, User, RecoveryCodeMethod, TOTPMethod
from uffd.models.mfa import _hotp
from uffd.views.session import login_required
from uffd.session.views import login_required from tests.utils import dump, UffdTestCase, db_flush
from uffd.session.models import DeviceLoginConfirmation
from uffd.oauth2.models import OAuth2DeviceLoginInitiation
from uffd import create_app, db
from utils import dump, UffdTestCase
class TestSession(UffdTestCase): class TestSession(UffdTestCase):
def setUpApp(self): def setUpApp(self):
...@@ -20,15 +18,15 @@ class TestSession(UffdTestCase): ...@@ -20,15 +18,15 @@ class TestSession(UffdTestCase):
@self.app.route('/test_login_required') @self.app.route('/test_login_required')
@login_required() @login_required()
def test_login_required(): def test_login_required():
return 'SUCCESS', 200 return 'SUCCESS ' + request.user.loginname, 200
@self.app.route('/test_group_required1') @self.app.route('/test_group_required1')
@login_required(group='users') @login_required(lambda: request.user.is_in_group('users'))
def test_group_required1(): def test_group_required1():
return 'SUCCESS', 200 return 'SUCCESS', 200
@self.app.route('/test_group_required2') @self.app.route('/test_group_required2')
@login_required(group='notagroup') @login_required(lambda: request.user.is_in_group('notagroup'))
def test_group_required2(): def test_group_required2():
return 'SUCCESS', 200 return 'SUCCESS', 200
...@@ -41,15 +39,10 @@ class TestSession(UffdTestCase): ...@@ -41,15 +39,10 @@ class TestSession(UffdTestCase):
self.assertIsNotNone(request.user) self.assertIsNotNone(request.user)
def assertLoggedIn(self): def assertLoggedIn(self):
self.assertIsNotNone(request.user) self.assertEqual(self.client.get(path=url_for('test_login_required'), follow_redirects=True).data, b'SUCCESS testuser')
self.assertEqual(self.client.get(path=url_for('test_login_required'), follow_redirects=True).data, b'SUCCESS')
self.assertEqual(request.user.loginname, self.get_user().loginname)
def assertLoggedOut(self): def assertLoggedOut(self):
self.assertIsNone(request.user) self.assertNotIn(b'SUCCESS', self.client.get(path=url_for('test_login_required'), follow_redirects=True).data)
self.assertNotEqual(self.client.get(path=url_for('test_login_required'),
follow_redirects=True).data, b'SUCCESS')
self.assertEqual(request.user, None)
def test_login(self): def test_login(self):
self.assertLoggedOut() self.assertLoggedOut()
...@@ -61,20 +54,31 @@ class TestSession(UffdTestCase): ...@@ -61,20 +54,31 @@ class TestSession(UffdTestCase):
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertLoggedIn() self.assertLoggedIn()
def test_login_password_rehash(self):
self.get_user().password = PlaintextPasswordHash.from_password('userpassword')
db.session.commit()
self.assertIsInstance(self.get_user().password, PlaintextPasswordHash)
db_flush()
r = self.login_as('user')
self.assertEqual(r.status_code, 200)
self.assertLoggedIn()
self.assertIsInstance(self.get_user().password, User.password.method_cls)
self.assertTrue(self.get_user().password.verify('userpassword'))
def test_titlecase_password(self): def test_titlecase_password(self):
r = self.client.post(path=url_for('session.login'), r = self.client.post(path=url_for('session.login'),
data={'loginname': self.test_data.get('user').get('loginname').title(), 'password': self.test_data.get('user').get('password')}, follow_redirects=True) data={'loginname': self.get_user().loginname.title(), 'password': 'userpassword'}, follow_redirects=True)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertLoggedIn() self.assertLoggedIn()
def test_redirect(self): def test_redirect(self):
r = self.login_as('user', ref=url_for('test_login_required')) r = self.login_as('user', ref=url_for('test_login_required'))
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, b'SUCCESS') self.assertEqual(r.data, b'SUCCESS testuser')
def test_wrong_password(self): def test_wrong_password(self):
r = self.client.post(path=url_for('session.login'), r = self.client.post(path=url_for('session.login'),
data={'loginname': self.test_data.get('user').get('loginname'), 'password': 'wrongpassword'}, data={'loginname': self.get_user().loginname, 'password': 'wrongpassword'},
follow_redirects=True) follow_redirects=True)
dump('login_wrong_password', r) dump('login_wrong_password', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
...@@ -82,14 +86,22 @@ class TestSession(UffdTestCase): ...@@ -82,14 +86,22 @@ class TestSession(UffdTestCase):
def test_empty_password(self): def test_empty_password(self):
r = self.client.post(path=url_for('session.login'), r = self.client.post(path=url_for('session.login'),
data={'loginname': self.test_data.get('user').get('loginname'), 'password': ''}, follow_redirects=True) data={'loginname': self.get_user().loginname, 'password': ''}, follow_redirects=True)
dump('login_empty_password', r) dump('login_empty_password', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertLoggedOut() self.assertLoggedOut()
# Regression test for #100 (uncatched LDAPSASLPrepError)
def test_saslprep_invalid_password(self):
r = self.client.post(path=url_for('session.login'),
data={'loginname': 'testuser', 'password': 'wrongpassword\n'}, follow_redirects=True)
dump('login_saslprep_invalid_password', r)
self.assertEqual(r.status_code, 200)
self.assertLoggedOut()
def test_wrong_user(self): def test_wrong_user(self):
r = self.client.post(path=url_for('session.login'), r = self.client.post(path=url_for('session.login'),
data={'loginname': 'nouser', 'password': self.test_data.get('user').get('password')}, data={'loginname': 'nouser', 'password': 'userpassword'},
follow_redirects=True) follow_redirects=True)
dump('login_wrong_user', r) dump('login_wrong_user', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
...@@ -97,7 +109,7 @@ class TestSession(UffdTestCase): ...@@ -97,7 +109,7 @@ class TestSession(UffdTestCase):
def test_empty_user(self): def test_empty_user(self):
r = self.client.post(path=url_for('session.login'), r = self.client.post(path=url_for('session.login'),
data={'loginname': '', 'password': self.test_data.get('user').get('password')}, follow_redirects=True) data={'loginname': '', 'password': 'userpassword'}, follow_redirects=True)
dump('login_empty_user', r) dump('login_empty_user', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertLoggedOut() self.assertLoggedOut()
...@@ -109,6 +121,20 @@ class TestSession(UffdTestCase): ...@@ -109,6 +121,20 @@ class TestSession(UffdTestCase):
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertLoggedOut() self.assertLoggedOut()
def test_deactivated(self):
self.get_user().is_deactivated = True
db.session.commit()
r = self.login_as('user')
dump('login_deactivated', r)
self.assertEqual(r.status_code, 200)
self.assertLoggedOut()
def test_deactivated_after_login(self):
self.login_as('user')
self.get_user().is_deactivated = True
db.session.commit()
self.assertLoggedOut()
def test_group_required(self): def test_group_required(self):
self.login() self.login()
self.assertEqual(self.client.get(path=url_for('test_group_required1'), self.assertEqual(self.client.get(path=url_for('test_group_required1'),
...@@ -123,7 +149,6 @@ class TestSession(UffdTestCase): ...@@ -123,7 +149,6 @@ class TestSession(UffdTestCase):
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertLoggedOut() self.assertLoggedOut()
@unittest.skip('See #29')
def test_timeout(self): def test_timeout(self):
self.login() self.login()
time.sleep(3) time.sleep(3)
...@@ -132,7 +157,7 @@ class TestSession(UffdTestCase): ...@@ -132,7 +157,7 @@ class TestSession(UffdTestCase):
def test_ratelimit(self): def test_ratelimit(self):
for i in range(20): for i in range(20):
self.client.post(path=url_for('session.login'), self.client.post(path=url_for('session.login'),
data={'loginname': self.test_data.get('user').get('loginname'), data={'loginname': self.get_user().loginname,
'password': 'wrongpassword_%i'%i}, follow_redirects=True) 'password': 'wrongpassword_%i'%i}, follow_redirects=True)
r = self.login_as('user') r = self.login_as('user')
dump('login_ratelimit', r) dump('login_ratelimit', r)
...@@ -140,10 +165,8 @@ class TestSession(UffdTestCase): ...@@ -140,10 +165,8 @@ class TestSession(UffdTestCase):
self.assertIsNone(request.user) self.assertIsNone(request.user)
def test_deviceauth(self): def test_deviceauth(self):
self.app.config['OAUTH2_CLIENTS'] = { oauth2_client = OAuth2Client(service=Service(name='test', limit_access=False), client_id='test', client_secret='testsecret', redirect_uris=['http://localhost:5009/callback', 'http://localhost:5009/callback2'])
'test': {'client_secret': 'testsecret', 'redirect_uris': ['http://localhost:5009/callback', 'http://localhost:5009/callback2']}, initiation = OAuth2DeviceLoginInitiation(client=oauth2_client)
}
initiation = OAuth2DeviceLoginInitiation(oauth2_client_id='test')
db.session.add(initiation) db.session.add(initiation)
db.session.commit() db.session.commit()
code = initiation.code code = initiation.code
...@@ -160,14 +183,162 @@ class TestSession(UffdTestCase): ...@@ -160,14 +183,162 @@ class TestSession(UffdTestCase):
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
initiation = OAuth2DeviceLoginInitiation.query.filter_by(code=code).one() initiation = OAuth2DeviceLoginInitiation.query.filter_by(code=code).one()
self.assertEqual(len(initiation.confirmations), 1) self.assertEqual(len(initiation.confirmations), 1)
self.assertEqual(initiation.confirmations[0].user.loginname, 'testuser') self.assertEqual(initiation.confirmations[0].session.user.loginname, 'testuser')
self.assertIn(initiation.confirmations[0].code.encode(), r.data) self.assertIn(initiation.confirmations[0].code.encode(), r.data)
r = self.client.get(path=url_for('session.deviceauth_finish'), follow_redirects=True) r = self.client.get(path=url_for('session.deviceauth_finish'), follow_redirects=True)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertEqual(DeviceLoginConfirmation.query.all(), []) self.assertEqual(DeviceLoginConfirmation.query.all(), [])
class TestSessionOL(TestSession): class TestMfaViews(UffdTestCase):
use_openldap = True def add_recovery_codes(self, count=10):
user = self.get_user()
for _ in range(count):
db.session.add(RecoveryCodeMethod(user=user))
db.session.commit()
def add_totp(self):
db.session.add(TOTPMethod(user=self.get_user(), name='My phone'))
db.session.commit()
def test_auth_integration(self):
self.add_recovery_codes()
self.add_totp()
db.session.commit()
self.assertIsNone(request.user)
r = self.login_as('user')
dump('mfa_auth_redirected', r)
self.assertEqual(r.status_code, 200)
self.assertIn(b'/mfa/auth', r.data)
self.assertIsNone(request.user)
r = self.client.get(path=url_for('session.mfa_auth'), follow_redirects=False)
dump('mfa_auth', r)
self.assertEqual(r.status_code, 200)
self.assertIsNone(request.user)
def test_auth_disabled(self):
self.assertIsNone(request.user)
self.login_as('user')
r = self.client.get(path=url_for('session.mfa_auth', ref='/redirecttarget'), follow_redirects=False)
self.assertEqual(r.status_code, 302)
self.assertTrue(r.location.endswith('/redirecttarget'))
self.assertIsNotNone(request.user)
def test_auth_recovery_only(self):
self.add_recovery_codes()
self.assertIsNone(request.user)
self.login_as('user')
r = self.client.get(path=url_for('session.mfa_auth', ref='/redirecttarget'), follow_redirects=False)
self.assertEqual(r.status_code, 302)
self.assertTrue(r.location.endswith('/redirecttarget'))
self.assertIsNotNone(request.user)
def test_auth_recovery_code(self):
self.add_recovery_codes()
self.add_totp()
method = RecoveryCodeMethod(user=self.get_user())
db.session.add(method)
db.session.commit()
method_id = method.id
self.login_as('user')
r = self.client.get(path=url_for('session.mfa_auth'), follow_redirects=False)
dump('mfa_auth_recovery_code', r)
self.assertEqual(r.status_code, 200)
self.assertIsNone(request.user)
r = self.client.post(path=url_for('session.mfa_auth_finish', ref='/redirecttarget'), data={'code': method.code_value})
self.assertEqual(r.status_code, 302)
self.assertTrue(r.location.endswith('/redirecttarget'))
self.assertIsNotNone(request.user)
self.assertEqual(len(RecoveryCodeMethod.query.filter_by(id=method_id).all()), 0)
def test_auth_totp_code(self):
self.add_recovery_codes()
self.add_totp()
method = TOTPMethod(user=self.get_user(), name='testname')
raw_key = method.raw_key
db.session.add(method)
db.session.commit()
self.login_as('user')
r = self.client.get(path=url_for('session.mfa_auth'), follow_redirects=False)
dump('mfa_auth_totp_code', r)
self.assertEqual(r.status_code, 200)
self.assertIsNone(request.user)
code = _hotp(int(time.time()/30), raw_key)
r = self.client.post(path=url_for('session.mfa_auth_finish'), data={'code': code}, follow_redirects=True)
dump('mfa_auth_totp_code_submit', r)
self.assertEqual(r.status_code, 200)
self.assertIsNotNone(request.user)
def test_auth_totp_code_reuse(self):
self.add_recovery_codes()
self.add_totp()
method = TOTPMethod(user=self.get_user(), name='testname')
raw_key = method.raw_key
db.session.add(method)
db.session.commit()
self.login_as('user')
r = self.client.get(path=url_for('session.mfa_auth'), follow_redirects=False)
self.assertEqual(r.status_code, 200)
self.assertIsNone(request.user)
code = _hotp(int(time.time()/30), raw_key)
r = self.client.post(path=url_for('session.mfa_auth_finish'), data={'code': code}, follow_redirects=True)
self.assertEqual(r.status_code, 200)
self.assertIsNotNone(request.user)
self.login_as('user')
r = self.client.get(path=url_for('session.mfa_auth'), follow_redirects=False)
self.assertEqual(r.status_code, 200)
self.assertIsNone(request.user)
r = self.client.post(path=url_for('session.mfa_auth_finish'), data={'code': code}, follow_redirects=True)
self.assertEqual(r.status_code, 200)
self.assertIsNone(request.user)
def test_auth_empty_code(self):
self.add_recovery_codes()
self.add_totp()
self.login_as('user')
r = self.client.get(path=url_for('session.mfa_auth'), follow_redirects=False)
self.assertEqual(r.status_code, 200)
self.assertIsNone(request.user)
r = self.client.post(path=url_for('session.mfa_auth_finish'), data={'code': ''}, follow_redirects=True)
dump('mfa_auth_empty_code', r)
self.assertEqual(r.status_code, 200)
self.assertIsNone(request.user)
def test_auth_invalid_code(self):
self.add_recovery_codes()
self.add_totp()
method = TOTPMethod(user=self.get_user(), name='testname')
raw_key = method.raw_key
db.session.add(method)
db.session.commit()
self.login_as('user')
r = self.client.get(path=url_for('session.mfa_auth'), follow_redirects=False)
self.assertEqual(r.status_code, 200)
self.assertIsNone(request.user)
code = _hotp(int(time.time()/30), raw_key)
code = str(int(code[0])+1)[-1] + code[1:]
r = self.client.post(path=url_for('session.mfa_auth_finish'), data={'code': code}, follow_redirects=True)
dump('mfa_auth_invalid_code', r)
self.assertEqual(r.status_code, 200)
self.assertIsNone(request.user)
def test_auth_ratelimit(self):
self.add_recovery_codes()
self.add_totp()
method = TOTPMethod(user=self.get_user(), name='testname')
raw_key = method.raw_key
db.session.add(method)
db.session.commit()
self.login_as('user')
self.assertIsNone(request.user)
code = _hotp(int(time.time()/30), raw_key)
inv_code = str(int(code[0])+1)[-1] + code[1:]
for i in range(20):
r = self.client.post(path=url_for('session.mfa_auth_finish'), data={'code': inv_code}, follow_redirects=True)
self.assertEqual(r.status_code, 200)
self.assertIsNone(request.user)
r = self.client.post(path=url_for('session.mfa_auth_finish'), data={'code': code}, follow_redirects=True)
dump('mfa_auth_ratelimit', r)
self.assertEqual(r.status_code, 200)
self.assertIsNone(request.user)
class TestSessionOLUser(TestSessionOL): # TODO: webauthn auth tests
use_userconnection = True
import unittest
import datetime import datetime
import time
from flask import url_for, session, request from flask import url_for, request
# These imports are required, because otherwise we get circular imports?! from uffd.database import db
from uffd import user from uffd.models import User, Signup, Role, RoleGroup, FeatureFlag
from uffd.ldap import ldap
from uffd import create_app, db from tests.utils import dump, UffdTestCase, db_flush
from uffd.signup.models import Signup
from uffd.user.models import User
from uffd.session.views import login_get_user
from utils import dump, UffdTestCase, db_flush
def refetch_signup(signup): def refetch_signup(signup):
db.session.add(signup) db.session.add(signup)
db.session.commit() db.session.commit()
token = signup.token id = signup.id
db_flush() db.session.expunge(signup)
return Signup.query.get(token) return Signup.query.get(id)
# We assume in all tests that Signup.validate and Signup.check_password do
# not alter any state
class TestSignupModel(UffdTestCase):
def assert_validate_valid(self, signup):
valid, msg = signup.validate()
self.assertTrue(valid)
self.assertIsInstance(msg, str)
def assert_validate_invalid(self, signup):
valid, msg = signup.validate()
self.assertFalse(valid)
self.assertIsInstance(msg, str)
self.assertNotEqual(msg, '')
def assert_finish_success(self, signup, password):
self.assertIsNone(signup.user)
user, msg = signup.finish(password)
ldap.session.commit()
self.assertIsNotNone(user)
self.assertIsInstance(msg, str)
self.assertIsNotNone(signup.user)
def assert_finish_failure(self, signup, password):
prev_dn = signup.user_dn
user, msg = signup.finish(password)
self.assertIsNone(user)
self.assertIsInstance(msg, str)
self.assertNotEqual(msg, '')
self.assertEqual(signup.user_dn, prev_dn)
def test_password(self):
signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com')
self.assertFalse(signup.check_password('notsecret'))
self.assertFalse(signup.check_password(''))
self.assertFalse(signup.check_password('wrongpassword'))
signup.password = 'notsecret'
self.assertTrue(signup.check_password('notsecret'))
self.assertFalse(signup.check_password('wrongpassword'))
def test_expired(self):
# TODO: Find a better way to test this!
signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret')
self.assertFalse(signup.expired)
signup.created = created=datetime.datetime.now() - datetime.timedelta(hours=49)
self.assertTrue(signup.expired)
def test_completed(self):
signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret')
self.assertFalse(signup.completed)
signup.finish('notsecret')
ldap.session.commit()
self.assertTrue(signup.completed)
signup = refetch_signup(signup)
self.assertTrue(signup.completed)
def test_validate(self):
signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret')
self.assert_validate_valid(signup)
self.assert_validate_valid(refetch_signup(signup))
def test_validate_completed(self):
signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret')
self.assert_finish_success(signup, 'notsecret')
self.assert_validate_invalid(signup)
self.assert_validate_invalid(refetch_signup(signup))
def test_validate_expired(self):
signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com',
password='notsecret', created=datetime.datetime.now()-datetime.timedelta(hours=49))
self.assert_validate_invalid(signup)
self.assert_validate_invalid(refetch_signup(signup))
def test_validate_loginname(self):
signup = Signup(loginname='', displayname='New User', mail='test@example.com', password='notsecret')
self.assert_validate_invalid(signup)
self.assert_validate_invalid(refetch_signup(signup))
def test_validate_displayname(self):
signup = Signup(loginname='newuser', displayname='', mail='test@example.com', password='notsecret')
self.assert_validate_invalid(signup)
self.assert_validate_invalid(refetch_signup(signup))
def test_validate_mail(self):
signup = Signup(loginname='newuser', displayname='New User', mail='', password='notsecret')
self.assert_validate_invalid(signup)
self.assert_validate_invalid(refetch_signup(signup))
def test_validate_password(self):
signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', password='')
self.assert_validate_invalid(signup)
self.assert_validate_invalid(refetch_signup(signup))
def test_validate_exists(self):
signup = Signup(loginname='testuser', displayname='New User', mail='test@example.com', password='notsecret')
self.assert_validate_invalid(signup)
self.assert_validate_invalid(refetch_signup(signup))
def test_finish(self):
signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret')
if self.use_openldap:
self.assertIsNone(login_get_user('newuser', 'notsecret'))
self.assert_finish_success(signup, 'notsecret')
user = User.query.get('uid=newuser,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE']))
self.assertEqual(user.loginname, 'newuser')
self.assertEqual(user.displayname, 'New User')
self.assertEqual(user.mail, 'test@example.com')
if self.use_openldap:
self.assertIsNotNone(login_get_user('newuser', 'notsecret'))
def test_finish_completed(self):
signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret')
self.assert_finish_success(signup, 'notsecret')
self.assert_finish_failure(refetch_signup(signup), 'notsecret')
def test_finish_expired(self):
# TODO: Find a better way to test this!
signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com',
password='notsecret', created=datetime.datetime.now()-datetime.timedelta(hours=49))
self.assert_finish_failure(signup, 'notsecret')
self.assert_finish_failure(refetch_signup(signup), 'notsecret')
def test_finish_wrongpassword(self):
signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com')
self.assert_finish_failure(signup, '')
self.assert_finish_failure(signup, 'wrongpassword')
signup = refetch_signup(signup)
self.assert_finish_failure(signup, '')
self.assert_finish_failure(signup, 'wrongpassword')
signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret')
self.assert_finish_failure(signup, 'wrongpassword')
self.assert_finish_failure(refetch_signup(signup), 'wrongpassword')
def test_finish_ldaperror(self):
signup = Signup(loginname='testuser', displayname='New User', mail='test@example.com', password='notsecret')
self.assert_finish_failure(signup, 'notsecret')
self.assert_finish_failure(refetch_signup(signup), 'notsecret')
def test_duplicate(self):
signup = Signup(loginname='newuser', displayname='New User', mail='test1@example.com', password='notsecret')
self.assert_validate_valid(signup)
db.session.add(signup)
db.session.commit()
signup1_token = signup.token
signup = Signup(loginname='newuser', displayname='New User', mail='test2@example.com', password='notsecret')
self.assert_validate_valid(signup)
db.session.add(signup)
db.session.commit()
signup2_token = signup.token
db_flush()
signup = Signup.query.get(signup2_token)
self.assert_finish_success(signup, 'notsecret')
db.session.commit()
db_flush()
signup = Signup.query.get(signup1_token)
self.assert_finish_failure(signup, 'notsecret')
user = User.query.get('uid=newuser,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE']))
self.assertEqual(user.mail, 'test2@example.com')
class TestSignupModelOL(TestSignupModel):
use_openldap = True
class TestSignupViews(UffdTestCase): class TestSignupViews(UffdTestCase):
def setUpApp(self): def setUpApp(self):
...@@ -195,7 +25,7 @@ class TestSignupViews(UffdTestCase): ...@@ -195,7 +25,7 @@ class TestSignupViews(UffdTestCase):
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertEqual(Signup.query.filter_by(loginname='newuser').all(), []) self.assertEqual(Signup.query.filter_by(loginname='newuser').all(), [])
r = self.client.post(path=url_for('signup.signup_submit'), follow_redirects=True, r = self.client.post(path=url_for('signup.signup_submit'), follow_redirects=True,
data={'loginname': 'newuser', 'displayname': 'New User', 'mail': 'test@example.com', data={'loginname': 'newuser', 'displayname': 'New User', 'mail': 'new@example.com',
'password1': 'notsecret', 'password2': 'notsecret'}) 'password1': 'notsecret', 'password2': 'notsecret'})
dump('test_signup_submit', r) dump('test_signup_submit', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
...@@ -205,9 +35,9 @@ class TestSignupViews(UffdTestCase): ...@@ -205,9 +35,9 @@ class TestSignupViews(UffdTestCase):
signup = signups[0] signup = signups[0]
self.assertEqual(signup.loginname, 'newuser') self.assertEqual(signup.loginname, 'newuser')
self.assertEqual(signup.displayname, 'New User') self.assertEqual(signup.displayname, 'New User')
self.assertEqual(signup.mail, 'test@example.com') self.assertEqual(signup.mail, 'new@example.com')
self.assertIn(signup.token, str(self.app.last_mail.get_content())) self.assertIn(signup.token, str(self.app.last_mail.get_content()))
self.assertTrue(signup.check_password('notsecret')) self.assertTrue(signup.password.verify('notsecret'))
self.assertTrue(signup.validate()[0]) self.assertTrue(signup.validate()[0])
def test_signup_disabled(self): def test_signup_disabled(self):
...@@ -217,7 +47,7 @@ class TestSignupViews(UffdTestCase): ...@@ -217,7 +47,7 @@ class TestSignupViews(UffdTestCase):
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertEqual(Signup.query.filter_by(loginname='newuser').all(), []) self.assertEqual(Signup.query.filter_by(loginname='newuser').all(), [])
r = self.client.post(path=url_for('signup.signup_submit'), follow_redirects=True, r = self.client.post(path=url_for('signup.signup_submit'), follow_redirects=True,
data={'loginname': 'newuser', 'displayname': 'New User', 'mail': 'test@example.com', data={'loginname': 'newuser', 'displayname': 'New User', 'mail': 'new@example.com',
'password1': 'notsecret', 'password2': 'notsecret'}) 'password1': 'notsecret', 'password2': 'notsecret'})
dump('test_signup_submit_disabled', r) dump('test_signup_submit_disabled', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
...@@ -226,7 +56,7 @@ class TestSignupViews(UffdTestCase): ...@@ -226,7 +56,7 @@ class TestSignupViews(UffdTestCase):
def test_signup_wrongpassword(self): def test_signup_wrongpassword(self):
r = self.client.post(path=url_for('signup.signup_submit'), follow_redirects=True, r = self.client.post(path=url_for('signup.signup_submit'), follow_redirects=True,
data={'loginname': 'newuser', 'displayname': 'New User', 'mail': 'test@example.com', data={'loginname': 'newuser', 'displayname': 'New User', 'mail': 'new@example.com',
'password1': 'notsecret', 'password2': 'notthesame'}) 'password1': 'notsecret', 'password2': 'notthesame'})
dump('test_signup_wrongpassword', r) dump('test_signup_wrongpassword', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
...@@ -234,7 +64,7 @@ class TestSignupViews(UffdTestCase): ...@@ -234,7 +64,7 @@ class TestSignupViews(UffdTestCase):
def test_signup_invalid(self): def test_signup_invalid(self):
r = self.client.post(path=url_for('signup.signup_submit'), follow_redirects=True, r = self.client.post(path=url_for('signup.signup_submit'), follow_redirects=True,
data={'loginname': '', 'displayname': 'New User', 'mail': 'test@example.com', data={'loginname': '', 'displayname': 'New User', 'mail': 'new@example.com',
'password1': 'notsecret', 'password2': 'notsecret'}) 'password1': 'notsecret', 'password2': 'notsecret'})
dump('test_signup_invalid', r) dump('test_signup_invalid', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
...@@ -243,7 +73,7 @@ class TestSignupViews(UffdTestCase): ...@@ -243,7 +73,7 @@ class TestSignupViews(UffdTestCase):
def test_signup_mailerror(self): def test_signup_mailerror(self):
self.app.config['MAIL_SKIP_SEND'] = 'fail' self.app.config['MAIL_SKIP_SEND'] = 'fail'
r = self.client.post(path=url_for('signup.signup_submit'), follow_redirects=True, r = self.client.post(path=url_for('signup.signup_submit'), follow_redirects=True,
data={'loginname': 'newuser', 'displayname': 'New User', 'mail': 'test@example.com', data={'loginname': 'newuser', 'displayname': 'New User', 'mail': 'new@example.com',
'password1': 'notsecret', 'password2': 'notsecret'}) 'password1': 'notsecret', 'password2': 'notsecret'})
dump('test_signup_mailerror', r) dump('test_signup_mailerror', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
...@@ -262,7 +92,7 @@ class TestSignupViews(UffdTestCase): ...@@ -262,7 +92,7 @@ class TestSignupViews(UffdTestCase):
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.app.last_mail = None self.app.last_mail = None
r = self.client.post(path=url_for('signup.signup_submit'), follow_redirects=True, r = self.client.post(path=url_for('signup.signup_submit'), follow_redirects=True,
data={'loginname': 'newuser', 'displayname': 'New User', 'mail': 'test@example.com', data={'loginname': 'newuser', 'displayname': 'New User', 'mail': 'new@example.com',
'password1': 'notsecret', 'password2': 'notsecret'}) 'password1': 'notsecret', 'password2': 'notsecret'})
dump('test_signup_hostlimit', r) dump('test_signup_hostlimit', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
...@@ -272,12 +102,12 @@ class TestSignupViews(UffdTestCase): ...@@ -272,12 +102,12 @@ class TestSignupViews(UffdTestCase):
def test_signup_maillimit(self): def test_signup_maillimit(self):
for i in range(3): for i in range(3):
r = self.client.post(path=url_for('signup.signup_submit'), follow_redirects=True, r = self.client.post(path=url_for('signup.signup_submit'), follow_redirects=True,
data={'loginname': 'newuser%d'%i, 'displayname': 'New User', 'mail': 'test@example.com', data={'loginname': 'newuser%d'%i, 'displayname': 'New User', 'mail': 'new@example.com',
'password1': 'notsecret', 'password2': 'notsecret'}) 'password1': 'notsecret', 'password2': 'notsecret'})
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.app.last_mail = None self.app.last_mail = None
r = self.client.post(path=url_for('signup.signup_submit'), follow_redirects=True, r = self.client.post(path=url_for('signup.signup_submit'), follow_redirects=True,
data={'loginname': 'newuser', 'displayname': 'New User', 'mail': 'test@example.com', data={'loginname': 'newuser', 'displayname': 'New User', 'mail': 'new@example.com',
'password1': 'notsecret', 'password2': 'notsecret'}) 'password1': 'notsecret', 'password2': 'notsecret'})
dump('test_signup_maillimit', r) dump('test_signup_maillimit', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
...@@ -323,41 +153,44 @@ class TestSignupViews(UffdTestCase): ...@@ -323,41 +153,44 @@ class TestSignupViews(UffdTestCase):
self.assertEqual(r.json['status'], 'ratelimited') self.assertEqual(r.json['status'], 'ratelimited')
def test_confirm(self): def test_confirm(self):
signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret') baserole = Role(name='baserole', is_default=True)
db.session.add(baserole)
baserole.groups[self.get_access_group()] = RoleGroup()
db.session.commit()
signup = Signup(loginname='newuser', displayname='New User', mail='new@example.com', password='notsecret')
signup = refetch_signup(signup) signup = refetch_signup(signup)
self.assertFalse(signup.completed) self.assertFalse(signup.completed)
if self.use_openldap: self.assertIsNone(User.query.filter_by(loginname='newuser').one_or_none())
self.assertIsNone(login_get_user('newuser', 'notsecret')) r = self.client.get(path=url_for('signup.signup_confirm', signup_id=signup.id, token=signup.token), follow_redirects=True)
r = self.client.get(path=url_for('signup.signup_confirm', token=signup.token), follow_redirects=True)
dump('test_signup_confirm', r) dump('test_signup_confirm', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
signup = refetch_signup(signup) signup = refetch_signup(signup)
self.assertFalse(signup.completed) self.assertFalse(signup.completed)
if self.use_openldap: self.assertIsNone(User.query.filter_by(loginname='newuser').one_or_none())
self.assertIsNone(login_get_user('newuser', 'notsecret')) r = self.client.post(path=url_for('signup.signup_confirm_submit', signup_id=signup.id, token=signup.token), follow_redirects=True, data={'password': 'notsecret'})
r = self.client.post(path=url_for('signup.signup_confirm_submit', token=signup.token), follow_redirects=True, data={'password': 'notsecret'})
dump('test_signup_confirm_submit', r) dump('test_signup_confirm_submit', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
signup = refetch_signup(signup) signup = refetch_signup(signup)
self.assertTrue(signup.completed) self.assertTrue(signup.completed)
self.assertEqual(signup.user.loginname, 'newuser') self.assertEqual(signup.user.loginname, 'newuser')
self.assertEqual(signup.user.displayname, 'New User') self.assertEqual(signup.user.displayname, 'New User')
self.assertEqual(signup.user.mail, 'test@example.com') self.assertEqual(signup.user.primary_email.address, 'new@example.com')
if self.use_openldap: self.assertTrue(User.query.filter_by(loginname='newuser').one_or_none().password.verify('notsecret'))
self.assertIsNotNone(login_get_user('newuser', 'notsecret'))
self.assertIsNotNone(request.user)
self.assertEqual(request.user.loginname, 'newuser')
def test_confirm_loggedin(self): def test_confirm_loggedin(self):
signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret') baserole = Role(name='baserole', is_default=True)
signup = refetch_signup(signup) db.session.add(baserole)
baserole.groups[self.get_access_group()] = RoleGroup()
db.session.commit()
self.login_as('user') self.login_as('user')
signup = Signup(loginname='newuser', displayname='New User', mail='new@example.com', password='notsecret')
signup = refetch_signup(signup)
self.assertFalse(signup.completed) self.assertFalse(signup.completed)
self.assertIsNotNone(request.user) self.assertIsNotNone(request.user)
self.assertEqual(request.user.loginname, self.get_user().loginname) self.assertEqual(request.user.loginname, self.get_user().loginname)
r = self.client.get(path=url_for('signup.signup_confirm', token=signup.token), follow_redirects=True) r = self.client.get(path=url_for('signup.signup_confirm', signup_id=signup.id, token=signup.token), follow_redirects=True)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
r = self.client.post(path=url_for('signup.signup_confirm_submit', token=signup.token), follow_redirects=True, data={'password': 'notsecret'}) r = self.client.post(path=url_for('signup.signup_confirm_submit', signup_id=signup.id, token=signup.token), follow_redirects=True, data={'password': 'notsecret'})
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
signup = refetch_signup(signup) signup = refetch_signup(signup)
self.assertTrue(signup.completed) self.assertTrue(signup.completed)
...@@ -365,79 +198,93 @@ class TestSignupViews(UffdTestCase): ...@@ -365,79 +198,93 @@ class TestSignupViews(UffdTestCase):
self.assertEqual(request.user.loginname, 'newuser') self.assertEqual(request.user.loginname, 'newuser')
def test_confirm_notfound(self): def test_confirm_notfound(self):
r = self.client.get(path=url_for('signup.signup_confirm', token='notasignuptoken'), follow_redirects=True) r = self.client.get(path=url_for('signup.signup_confirm', signup_id=1, token='notasignuptoken'), follow_redirects=True)
dump('test_signup_confirm_notfound', r) dump('test_signup_confirm_notfound', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
r = self.client.post(path=url_for('signup.signup_confirm_submit', token='notasignuptoken'), follow_redirects=True, data={'password': 'notsecret'}) r = self.client.post(path=url_for('signup.signup_confirm_submit', signup_id=1, token='notasignuptoken'), follow_redirects=True, data={'password': 'notsecret'})
dump('test_signup_confirm_submit_notfound', r) dump('test_signup_confirm_submit_notfound', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
def test_confirm_expired(self): def test_confirm_expired(self):
signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret') signup = Signup(loginname='newuser', displayname='New User', mail='new@example.com', password='notsecret')
signup.created = datetime.datetime.now() - datetime.timedelta(hours=49) signup.created = datetime.datetime.utcnow() - datetime.timedelta(hours=49)
signup = refetch_signup(signup) signup = refetch_signup(signup)
r = self.client.get(path=url_for('signup.signup_confirm', token=signup.token), follow_redirects=True) r = self.client.get(path=url_for('signup.signup_confirm', signup_id=signup.id, token=signup.token), follow_redirects=True)
dump('test_signup_confirm_expired', r) dump('test_signup_confirm_expired', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
r = self.client.post(path=url_for('signup.signup_confirm_submit', token=signup.token), follow_redirects=True, data={'password': 'notsecret'}) r = self.client.post(path=url_for('signup.signup_confirm_submit', signup_id=signup.id, token=signup.token), follow_redirects=True, data={'password': 'notsecret'})
dump('test_signup_confirm_submit_expired', r) dump('test_signup_confirm_submit_expired', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
def test_confirm_completed(self): def test_confirm_completed(self):
signup = Signup(loginname=self.get_user().loginname, displayname='New User', mail='test@example.com', password='notsecret') signup = Signup(loginname=self.get_user().loginname, displayname='New User', mail='new@example.com', password='notsecret')
signup.user = self.get_user() signup.user = self.get_user()
signup = refetch_signup(signup) signup = refetch_signup(signup)
self.assertTrue(signup.completed) self.assertTrue(signup.completed)
r = self.client.get(path=url_for('signup.signup_confirm', token=signup.token), follow_redirects=True) r = self.client.get(path=url_for('signup.signup_confirm', signup_id=signup.id, token=signup.token), follow_redirects=True)
dump('test_signup_confirm_completed', r) dump('test_signup_confirm_completed', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
r = self.client.post(path=url_for('signup.signup_confirm_submit', token=signup.token), follow_redirects=True, data={'password': 'notsecret'}) r = self.client.post(path=url_for('signup.signup_confirm_submit', signup_id=signup.id, token=signup.token), follow_redirects=True, data={'password': 'notsecret'})
dump('test_signup_confirm_submit_completed', r) dump('test_signup_confirm_submit_completed', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
def test_confirm_wrongpassword(self): def test_confirm_wrongpassword(self):
signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret') signup = Signup(loginname='newuser', displayname='New User', mail='new@example.com', password='notsecret')
signup = refetch_signup(signup) signup = refetch_signup(signup)
r = self.client.post(path=url_for('signup.signup_confirm_submit', token=signup.token), follow_redirects=True, data={'password': 'wrongpassword'}) r = self.client.post(path=url_for('signup.signup_confirm_submit', signup_id=signup.id, token=signup.token), follow_redirects=True, data={'password': 'wrongpassword'})
dump('test_signup_confirm_wrongpassword', r) dump('test_signup_confirm_wrongpassword', r)
signup = refetch_signup(signup)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertFalse(signup.completed) self.assertFalse(signup.completed)
def test_confirm_error(self): def test_confirm_error(self):
# finish returns None and error message (here: because the user already exists) # finish returns None and error message (here: because the user already exists)
signup = Signup(loginname=self.get_user().loginname, displayname='New User', mail='test@example.com', password='notsecret') signup = Signup(loginname=self.get_user().loginname, displayname='New User', mail='new@example.com', password='notsecret')
signup = refetch_signup(signup) signup = refetch_signup(signup)
r = self.client.post(path=url_for('signup.signup_confirm_submit', token=signup.token), follow_redirects=True, data={'password': 'notsecret'}) r = self.client.post(path=url_for('signup.signup_confirm_submit', signup_id=signup.id, token=signup.token), follow_redirects=True, data={'password': 'notsecret'})
dump('test_signup_confirm_error', r) dump('test_signup_confirm_error', r)
signup = refetch_signup(signup)
self.assertEqual(r.status_code, 200)
self.assertFalse(signup.completed)
def test_confirm_error_email_uniqueness(self):
FeatureFlag.unique_email_addresses.enable()
db.session.commit()
# finish returns None and error message (here: because the email address already exists)
# This case is interesting, because the error also invalidates the ORM session
signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret')
db.session.add(signup)
db.session.commit()
signup_id = signup.id
r = self.client.post(path=url_for('signup.signup_confirm_submit', signup_id=signup.id, token=signup.token), follow_redirects=True, data={'password': 'notsecret'})
dump('test_signup_confirm_error_email_uniqueness', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
signup = Signup.query.get(signup_id)
self.assertFalse(signup.completed) self.assertFalse(signup.completed)
def test_confirm_hostlimit(self): def test_confirm_hostlimit(self):
for i in range(20): for i in range(20):
signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret') signup = Signup(loginname='newuser', displayname='New User', mail='new@example.com', password='notsecret')
signup = refetch_signup(signup) signup = refetch_signup(signup)
r = self.client.post(path=url_for('signup.signup_confirm_submit', token=signup.token), follow_redirects=True, data={'password': 'wrongpassword%d'%i}) r = self.client.post(path=url_for('signup.signup_confirm_submit', signup_id=signup.id, token=signup.token), follow_redirects=True, data={'password': 'wrongpassword%d'%i})
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret') signup = Signup(loginname='newuser', displayname='New User', mail='new@example.com', password='notsecret')
signup = refetch_signup(signup) signup = refetch_signup(signup)
self.assertFalse(signup.completed) self.assertFalse(signup.completed)
r = self.client.post(path=url_for('signup.signup_confirm_submit', token=signup.token), follow_redirects=True, data={'password': 'notsecret'}) r = self.client.post(path=url_for('signup.signup_confirm_submit', signup_id=signup.id, token=signup.token), follow_redirects=True, data={'password': 'notsecret'})
dump('test_signup_confirm_hostlimit', r) dump('test_signup_confirm_hostlimit', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertFalse(signup.completed) self.assertFalse(signup.completed)
def test_confirm_confirmlimit(self): def test_confirm_confirmlimit(self):
signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret') signup = Signup(loginname='newuser', displayname='New User', mail='new@example.com', password='notsecret')
signup = refetch_signup(signup) signup = refetch_signup(signup)
self.assertFalse(signup.completed) self.assertFalse(signup.completed)
for i in range(5): for i in range(5):
r = self.client.post(path=url_for('signup.signup_confirm_submit', token=signup.token), follow_redirects=True, data={'password': 'wrongpassword%d'%i}) r = self.client.post(path=url_for('signup.signup_confirm_submit', signup_id=signup.id, token=signup.token), follow_redirects=True, data={'password': 'wrongpassword%d'%i})
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertFalse(signup.completed) self.assertFalse(signup.completed)
r = self.client.post(path=url_for('signup.signup_confirm_submit', token=signup.token), follow_redirects=True, data={'password': 'notsecret'}) r = self.client.post(path=url_for('signup.signup_confirm_submit', signup_id=signup.id, token=signup.token), follow_redirects=True, data={'password': 'notsecret'})
dump('test_signup_confirm_confirmlimit', r) dump('test_signup_confirm_confirmlimit', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertFalse(signup.completed) self.assertFalse(signup.completed)
class TestSignupViewsOL(TestSignupViews):
use_openldap = True
import datetime from flask import url_for, request
import unittest
from flask import url_for, session
# These imports are required, because otherwise we get circular imports?!
from uffd import ldap, user
from uffd.user.models import User
from uffd.role.models import Role
from uffd import create_app, db
from utils import dump, UffdTestCase
class TestUserModel(UffdTestCase):
def test_has_permission(self):
user_ = self.get_user() # has 'users' and 'uffd_access' group
admin = self.get_admin() # has 'users', 'uffd_access' and 'uffd_admin' group
self.assertTrue(user_.has_permission(None))
self.assertTrue(admin.has_permission(None))
self.assertTrue(user_.has_permission('users'))
self.assertTrue(admin.has_permission('users'))
self.assertFalse(user_.has_permission('notagroup'))
self.assertFalse(admin.has_permission('notagroup'))
self.assertFalse(user_.has_permission('uffd_admin'))
self.assertTrue(admin.has_permission('uffd_admin'))
self.assertFalse(user_.has_permission(['uffd_admin']))
self.assertTrue(admin.has_permission(['uffd_admin']))
self.assertFalse(user_.has_permission(['uffd_admin', 'notagroup']))
self.assertTrue(admin.has_permission(['uffd_admin', 'notagroup']))
self.assertFalse(user_.has_permission(['notagroup', 'uffd_admin']))
self.assertTrue(admin.has_permission(['notagroup', 'uffd_admin']))
self.assertTrue(user_.has_permission(['uffd_admin', 'users']))
self.assertTrue(admin.has_permission(['uffd_admin', 'users']))
self.assertTrue(user_.has_permission([['uffd_admin', 'users'], ['users', 'uffd_access']]))
self.assertTrue(admin.has_permission([['uffd_admin', 'users'], ['users', 'uffd_access']]))
self.assertFalse(user_.has_permission(['uffd_admin', ['users', 'notagroup']]))
self.assertTrue(admin.has_permission(['uffd_admin', ['users', 'notagroup']]))
class TestUserModelOL(TestUserModel):
use_openldap = True
class TestUserModelOLUser(TestUserModelOL):
use_userconnection = True
def setUp(self): from uffd.database import db
super().setUp() from uffd.models import User, UserEmail, Group, Role, Service, ServiceUser, FeatureFlag, MFAMethod, RecoveryCodeMethod, TOTPMethod
self.login_as('admin')
from tests.utils import dump, UffdTestCase
class TestUserViews(UffdTestCase): class TestUserViews(UffdTestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.app.last_mail = None
self.login_as('admin') self.login_as('admin')
def test_index(self): def test_index(self):
...@@ -69,24 +27,25 @@ class TestUserViews(UffdTestCase): ...@@ -69,24 +27,25 @@ class TestUserViews(UffdTestCase):
r = self.client.get(path=url_for('user.show'), follow_redirects=True) r = self.client.get(path=url_for('user.show'), follow_redirects=True)
dump('user_new', r) dump('user_new', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertIsNone(User.query.get('uid=newuser,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE']))) self.assertIsNone(User.query.filter_by(loginname='newuser').one_or_none())
r = self.client.post(path=url_for('user.update'), r = self.client.post(path=url_for('user.create'),
data={'loginname': 'newuser', 'mail': 'newuser@example.com', 'displayname': 'New User', data={'loginname': 'newuser', 'email': 'newuser@example.com', 'displayname': 'New User',
f'role-{role1_id}': '1', 'password': 'newpassword'}, follow_redirects=True) f'role-{role1_id}': '1', 'password': 'newpassword'}, follow_redirects=True)
dump('user_new_submit', r) dump('user_new_submit', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
user_ = User.query.get('uid=newuser,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) user_ = User.query.filter_by(loginname='newuser').one_or_none()
roles = sorted([r.name for r in user_.roles_effective]) roles = sorted([r.name for r in user_.roles_effective])
self.assertIsNotNone(user_) self.assertIsNotNone(user_)
self.assertFalse(user_.is_service_user) self.assertFalse(user_.is_service_user)
self.assertEqual(user_.loginname, 'newuser') self.assertEqual(user_.loginname, 'newuser')
self.assertEqual(user_.displayname, 'New User') self.assertEqual(user_.displayname, 'New User')
self.assertEqual(user_.mail, 'newuser@example.com') self.assertEqual(user_.primary_email.address, 'newuser@example.com')
self.assertTrue(user_.uid) self.assertFalse(user_.password)
self.assertGreaterEqual(user_.unix_uid, self.app.config['USER_MIN_UID'])
self.assertLessEqual(user_.unix_uid, self.app.config['USER_MAX_UID'])
role1 = Role(name='role1') role1 = Role(name='role1')
self.assertEqual(roles, ['base', 'role1']) self.assertEqual(roles, ['base', 'role1'])
# TODO: confirm Mail is send, login not yet possible self.assertIsNotNone(self.app.last_mail)
#self.assertTrue(ldap.test_user_bind(user_.dn, 'newpassword'))
def test_new_service(self): def test_new_service(self):
db.session.add(Role(name='base', is_default=True)) db.session.add(Role(name='base', is_default=True))
...@@ -99,157 +58,341 @@ class TestUserViews(UffdTestCase): ...@@ -99,157 +58,341 @@ class TestUserViews(UffdTestCase):
r = self.client.get(path=url_for('user.show'), follow_redirects=True) r = self.client.get(path=url_for('user.show'), follow_redirects=True)
dump('user_new_service', r) dump('user_new_service', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertIsNone(User.query.get('uid=newuser,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE']))) self.assertIsNone(User.query.filter_by(loginname='newuser').one_or_none())
r = self.client.post(path=url_for('user.update'), r = self.client.post(path=url_for('user.create'),
data={'loginname': 'newuser', 'mail': 'newuser@example.com', 'displayname': 'New User', data={'loginname': 'newuser', 'email': 'newuser@example.com', 'displayname': 'New User',
f'role-{role1_id}': '1', 'password': 'newpassword', 'serviceaccount': '1'}, follow_redirects=True) f'role-{role1_id}': '1', 'password': 'newpassword', 'serviceaccount': '1'}, follow_redirects=True)
dump('user_new_submit', r) dump('user_new_submit', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
user = User.query.get('uid=newuser,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) user = User.query.filter_by(loginname='newuser').one_or_none()
roles = sorted([r.name for r in user.roles]) roles = sorted([r.name for r in user.roles])
self.assertIsNotNone(user) self.assertIsNotNone(user)
self.assertTrue(user.is_service_user) self.assertTrue(user.is_service_user)
self.assertEqual(user.loginname, 'newuser') self.assertEqual(user.loginname, 'newuser')
self.assertEqual(user.displayname, 'New User') self.assertEqual(user.displayname, 'New User')
self.assertEqual(user.mail, 'newuser@example.com') self.assertEqual(user.primary_email.address, 'newuser@example.com')
self.assertTrue(user.uid) self.assertTrue(user.unix_uid)
self.assertFalse(user.password)
role1 = Role(name='role1') role1 = Role(name='role1')
self.assertEqual(roles, ['role1']) self.assertEqual(roles, ['role1'])
# TODO: confirm Mail is send, login not yet possible self.assertIsNone(self.app.last_mail)
#self.assertTrue(ldap.test_user_bind(user.dn, 'newpassword'))
def test_new_invalid_loginname(self): def test_new_invalid_loginname(self):
r = self.client.post(path=url_for('user.update'), r = self.client.post(path=url_for('user.create'),
data={'loginname': '!newuser', 'mail': 'newuser@example.com', 'displayname': 'New User', data={'loginname': '!newuser', 'email': 'newuser@example.com', 'displayname': 'New User',
'password': 'newpassword'}, follow_redirects=True) 'password': 'newpassword'}, follow_redirects=True)
dump('user_new_invalid_loginname', r) dump('user_new_invalid_loginname', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertIsNone(User.query.get('uid=newuser,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE']))) self.assertIsNone(User.query.filter_by(loginname='newuser').one_or_none())
def test_new_empty_loginname(self): def test_new_empty_loginname(self):
r = self.client.post(path=url_for('user.update'), r = self.client.post(path=url_for('user.create'),
data={'loginname': '', 'mail': 'newuser@example.com', 'displayname': 'New User', data={'loginname': '', 'email': 'newuser@example.com', 'displayname': 'New User',
'password': 'newpassword'}, follow_redirects=True) 'password': 'newpassword'}, follow_redirects=True)
dump('user_new_empty_loginname', r) dump('user_new_empty_loginname', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertIsNone(User.query.get('uid=newuser,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE']))) self.assertIsNone(User.query.filter_by(loginname='newuser').one_or_none())
def test_new_conflicting_loginname(self):
self.assertEqual(User.query.filter_by(loginname='testuser').count(), 1)
r = self.client.post(path=url_for('user.create'),
data={'loginname': 'testuser', 'email': 'newuser@example.com', 'displayname': 'New User',
'password': 'newpassword'}, follow_redirects=True)
dump('user_new_conflicting_loginname', r)
self.assertEqual(r.status_code, 200)
self.assertEqual(User.query.filter_by(loginname='testuser').count(), 1)
def test_new_empty_email(self): def test_new_empty_email(self):
r = self.client.post(path=url_for('user.update'), r = self.client.post(path=url_for('user.create'),
data={'loginname': 'newuser', 'mail': '', 'displayname': 'New User', data={'loginname': 'newuser', 'email': '', 'displayname': 'New User',
'password': 'newpassword'}, follow_redirects=True) 'password': 'newpassword'}, follow_redirects=True)
dump('user_new_empty_email', r) dump('user_new_empty_email', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertIsNone(User.query.get('uid=newuser,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE']))) self.assertIsNone(User.query.filter_by(loginname='newuser').one_or_none())
def test_new_conflicting_email(self):
FeatureFlag.unique_email_addresses.enable()
db.session.commit()
r = self.client.post(path=url_for('user.create'),
data={'loginname': 'newuser', 'email': 'test@example.com', 'displayname': 'New User',
'password': 'newpassword'}, follow_redirects=True)
dump('user_new_conflicting_email', r)
self.assertEqual(r.status_code, 200)
self.assertIsNone(User.query.filter_by(loginname='newuser').one_or_none())
def test_new_invalid_display_name(self): def test_new_invalid_display_name(self):
r = self.client.post(path=url_for('user.update'), r = self.client.post(path=url_for('user.create'),
data={'loginname': 'newuser', 'mail': 'newuser@example.com', 'displayname': 'A'*200, data={'loginname': 'newuser', 'email': 'newuser@example.com', 'displayname': 'A'*200,
'password': 'newpassword'}, follow_redirects=True) 'password': 'newpassword'}, follow_redirects=True)
dump('user_new_invalid_display_name', r) dump('user_new_invalid_display_name', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertIsNone(User.query.get('uid=newuser,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE']))) self.assertIsNone(User.query.filter_by(loginname='newuser').one_or_none())
def test_update(self): def test_update(self):
user_unupdated = self.get_user() user_unupdated = self.get_user()
email_id = str(user_unupdated.primary_email.id)
db.session.add(Role(name='base', is_default=True)) db.session.add(Role(name='base', is_default=True))
role1 = Role(name='role1') role1 = Role(name='role1')
db.session.add(role1) db.session.add(role1)
role2 = Role(name='role2') role2 = Role(name='role2')
db.session.add(role2) db.session.add(role2)
role2.members.add(user_unupdated) role2.members.append(user_unupdated)
db.session.commit() db.session.commit()
role1_id = role1.id role1_id = role1.id
r = self.client.get(path=url_for('user.show', uid=user_unupdated.uid), follow_redirects=True) r = self.client.get(path=url_for('user.show', id=user_unupdated.id), follow_redirects=True)
dump('user_update', r) dump('user_update', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
r = self.client.post(path=url_for('user.update', uid=user_unupdated.uid), r = self.client.post(path=url_for('user.update', id=user_unupdated.id),
data={'loginname': 'testuser', 'mail': 'newuser@example.com', 'displayname': 'New User', data={'loginname': 'testuser',
f'role-{role1_id}': '1', 'password': ''}, follow_redirects=True) f'email-{email_id}-present': '1', 'primary_email': email_id, 'recovery_email': 'primary',
'displayname': 'New User', f'role-{role1_id}': '1', 'password': ''},
follow_redirects=True)
dump('user_update_submit', r) dump('user_update_submit', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
user_updated = self.get_user() user_updated = self.get_user()
roles = sorted([r.name for r in user_updated.roles_effective]) roles = sorted([r.name for r in user_updated.roles_effective])
self.assertEqual(user_updated.displayname, 'New User') self.assertEqual(user_updated.displayname, 'New User')
self.assertEqual(user_updated.mail, 'newuser@example.com') self.assertEqual(user_updated.primary_email.address, 'test@example.com')
self.assertEqual(user_updated.uid, user_unupdated.uid) self.assertEqual(user_updated.unix_uid, user_unupdated.unix_uid)
self.assertEqual(user_updated.loginname, user_unupdated.loginname) self.assertEqual(user_updated.loginname, user_unupdated.loginname)
print(user_updated.dn) self.assertTrue(user_updated.password.verify('userpassword'))
self.assertTrue(ldap.test_user_bind(user_updated.dn, self.test_data.get('user').get('password')))
self.assertEqual(roles, ['base', 'role1']) self.assertEqual(roles, ['base', 'role1'])
def test_update_password(self): def test_update_password(self):
user_unupdated = self.get_user() user_unupdated = self.get_user()
r = self.client.get(path=url_for('user.show', uid=user_unupdated.uid), follow_redirects=True) email_id = str(user_unupdated.primary_email.id)
r = self.client.get(path=url_for('user.show', id=user_unupdated.id), follow_redirects=True)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
r = self.client.post(path=url_for('user.update', uid=user_unupdated.uid), r = self.client.post(path=url_for('user.update', id=user_unupdated.id),
data={'loginname': 'testuser', 'mail': 'newuser@example.com', 'displayname': 'New User', data={'loginname': 'testuser',
f'email-{email_id}-present': '1', 'primary_email': email_id, 'recovery_email': 'primary',
'displayname': 'New User',
'password': 'newpassword'}, follow_redirects=True) 'password': 'newpassword'}, follow_redirects=True)
dump('user_update_password', r) dump('user_update_password', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
user_updated = self.get_user() user_updated = self.get_user()
self.assertEqual(user_updated.displayname, 'New User') self.assertEqual(user_updated.displayname, 'New User')
self.assertEqual(user_updated.mail, 'newuser@example.com') self.assertEqual(user_updated.primary_email.address, 'test@example.com')
self.assertEqual(user_updated.uid, user_unupdated.uid) self.assertEqual(user_updated.unix_uid, user_unupdated.unix_uid)
self.assertEqual(user_updated.loginname, user_unupdated.loginname) self.assertEqual(user_updated.loginname, user_unupdated.loginname)
self.assertTrue(ldap.test_user_bind(user_updated.dn, 'newpassword')) self.assertTrue(user_updated.password.verify('newpassword'))
self.assertFalse(user_updated.password.verify('userpassword'))
def test_update_invalid_password(self): def test_update_invalid_password(self):
user_unupdated = self.get_user() user_unupdated = self.get_user()
r = self.client.get(path=url_for('user.show', uid=user_unupdated.uid), follow_redirects=True) email_id = str(user_unupdated.primary_email.id)
r = self.client.get(path=url_for('user.show', id=user_unupdated.id), follow_redirects=True)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
r = self.client.post(path=url_for('user.update', uid=user_unupdated.uid), r = self.client.post(path=url_for('user.update', id=user_unupdated.id),
data={'loginname': 'testuser', 'mail': 'newuser@example.com', 'displayname': 'New User', data={'loginname': 'testuser',
f'email-{email_id}-present': '1', 'primary_email': email_id, 'recovery_email': 'primary',
'displayname': 'New User',
'password': 'A'}, follow_redirects=True) 'password': 'A'}, follow_redirects=True)
dump('user_update_password', r) dump('user_update_invalid_password', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
user_updated = self.get_user() user_updated = self.get_user()
self.assertFalse(ldap.test_user_bind(user_updated.dn, 'A')) self.assertFalse(user_updated.password.verify('A'))
self.assertTrue(user_updated.password.verify('userpassword'))
self.assertEqual(user_updated.displayname, user_unupdated.displayname) self.assertEqual(user_updated.displayname, user_unupdated.displayname)
self.assertEqual(user_updated.mail, user_unupdated.mail) self.assertEqual(user_updated.primary_email.address, user_unupdated.primary_email.address)
self.assertEqual(user_updated.loginname, user_unupdated.loginname) self.assertEqual(user_updated.loginname, user_unupdated.loginname)
def test_update_empty_email(self): # Regression test for #100 (login not possible if password contains character disallowed by SASLprep)
def test_update_saslprep_invalid_password(self):
user_unupdated = self.get_user() user_unupdated = self.get_user()
r = self.client.get(path=url_for('user.show', uid=user_unupdated.uid), follow_redirects=True) email_id = str(user_unupdated.primary_email.id)
r = self.client.get(path=url_for('user.show', id=user_unupdated.id), follow_redirects=True)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
r = self.client.post(path=url_for('user.update', uid=user_unupdated.uid), r = self.client.post(path=url_for('user.update', id=user_unupdated.id),
data={'loginname': 'testuser', 'mail': '', 'displayname': 'New User', data={'loginname': 'testuser',
'password': 'newpassword'}, follow_redirects=True) f'email-{email_id}-present': '1', 'primary_email': email_id, 'recovery_email': 'primary',
dump('user_update_empty_mail', r) 'displayname': 'New User',
'password': 'newpassword\n'}, follow_redirects=True)
dump('user_update_invalid_password', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
user_updated = self.get_user() user_updated = self.get_user()
self.assertFalse(user_updated.password.verify('newpassword\n'))
self.assertTrue(user_updated.password.verify('userpassword'))
self.assertEqual(user_updated.displayname, user_unupdated.displayname) self.assertEqual(user_updated.displayname, user_unupdated.displayname)
self.assertEqual(user_updated.mail, user_unupdated.mail) self.assertEqual(user_updated.primary_email.address, user_unupdated.primary_email.address)
self.assertEqual(user_updated.loginname, user_unupdated.loginname) self.assertEqual(user_updated.loginname, user_unupdated.loginname)
self.assertFalse(ldap.test_user_bind(user_updated.dn, 'newpassword'))
def test_update_email(self):
user = self.get_user()
email = UserEmail(user=user, address='foo@example.com')
service1 = Service(name='service1', enable_email_preferences=True, limit_access=False)
service2 = Service(name='service2', enable_email_preferences=True, limit_access=False)
db.session.add_all([service1, service2])
db.session.commit()
email1_id = user.primary_email.id
email2_id = email.id
service1_id = service1.id
service2_id = service2.id
r = self.client.post(path=url_for('user.update', id=user.id),
data={'loginname': 'testuser',
f'email-{email1_id}-present': '1',
f'email-{email2_id}-present': '1',
f'email-{email2_id}-verified': '1',
f'newemail-1-address': 'new1@example.com',
f'newemail-2-address': 'new2@example.com', f'newemail-2-verified': '1',
'primary_email': email2_id, 'recovery_email': email1_id,
f'service_{service1_id}_email': 'primary',
f'service_{service2_id}_email': email2_id,
'displayname': 'Test User', 'password': ''},
follow_redirects=True)
dump('user_update_email', r)
self.assertEqual(r.status_code, 200)
user = self.get_user()
self.assertEqual(user.primary_email.id, email2_id)
self.assertEqual(user.recovery_email.id, email1_id)
self.assertEqual(ServiceUser.query.get((service1.id, user.id)).service_email, None)
self.assertEqual(ServiceUser.query.get((service2.id, user.id)).service_email.id, email2_id)
self.assertEqual(
{email.address: email.verified for email in user.all_emails},
{
'test@example.com': True,
'foo@example.com': True,
'new1@example.com': False,
'new2@example.com': True,
}
)
def test_update_email_conflict(self):
user = self.get_user()
user_id = user.id
email_id = user.primary_email.id
email_address = user.primary_email.address
r = self.client.post(path=url_for('user.update', id=user.id),
data={'loginname': 'testuser',
f'email-{email_id}-present': '1',
f'newemail-1-address': user.primary_email.address},
follow_redirects=True)
dump('user_update_email_conflict', r)
self.assertEqual(r.status_code, 200)
self.assertEqual(UserEmail.query.filter_by(user_id=user_id).count(), 1)
def test_update_email_strict_uniqueness(self):
FeatureFlag.unique_email_addresses.enable()
db.session.commit()
user = self.get_user()
email = UserEmail(user=user, address='foo@example.com')
service1 = Service(name='service1', enable_email_preferences=True, limit_access=False)
service2 = Service(name='service2', enable_email_preferences=True, limit_access=False)
db.session.add_all([service1, service2])
db.session.commit()
email1_id = user.primary_email.id
email2_id = email.id
service1_id = service1.id
service2_id = service2.id
r = self.client.post(path=url_for('user.update', id=user.id),
data={'loginname': 'testuser',
f'email-{email1_id}-present': '1',
f'email-{email2_id}-present': '1',
f'email-{email2_id}-verified': '1',
f'newemail-1-address': 'new1@example.com',
f'newemail-2-address': 'new2@example.com', f'newemail-2-verified': '1',
'primary_email': email2_id, 'recovery_email': email1_id,
f'service_{service1_id}_email': 'primary',
f'service_{service2_id}_email': email2_id,
'displayname': 'Test User', 'password': ''},
follow_redirects=True)
dump('user_update_email_strict_uniqueness', r)
self.assertEqual(r.status_code, 200)
user = self.get_user()
self.assertEqual(user.primary_email.id, email2_id)
self.assertEqual(user.recovery_email.id, email1_id)
self.assertEqual(ServiceUser.query.get((service1.id, user.id)).service_email, None)
self.assertEqual(ServiceUser.query.get((service2.id, user.id)).service_email.id, email2_id)
self.assertEqual(
{email.address: email.verified for email in user.all_emails},
{
'test@example.com': True,
'foo@example.com': True,
'new1@example.com': False,
'new2@example.com': True,
}
)
def test_update_email_strict_uniqueness_conflict(self):
FeatureFlag.unique_email_addresses.enable()
db.session.commit()
user = self.get_user()
user_id = user.id
email_id = user.primary_email.id
email_address = user.primary_email.address
r = self.client.post(path=url_for('user.update', id=user.id),
data={'loginname': 'testuser',
f'email-{email_id}-present': '1',
f'newemail-1-address': user.primary_email.address},
follow_redirects=True)
dump('user_update_email_strict_uniqueness_conflict', r)
self.assertEqual(r.status_code, 200)
self.assertEqual(UserEmail.query.filter_by(user_id=user_id).count(), 1)
def test_update_invalid_display_name(self): def test_update_invalid_display_name(self):
user_unupdated = self.get_user() user_unupdated = self.get_user()
r = self.client.get(path=url_for('user.show', uid=user_unupdated.uid), follow_redirects=True) email_id = str(user_unupdated.primary_email.id)
r = self.client.get(path=url_for('user.show', id=user_unupdated.id), follow_redirects=True)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
r = self.client.post(path=url_for('user.update', uid=user_unupdated.uid), r = self.client.post(path=url_for('user.update', id=user_unupdated.id),
data={'loginname': 'testuser', 'mail': 'newuser@example.com', 'displayname': 'A'*200, data={'loginname': 'testuser',
f'email-{email_id}-present': '1', 'primary_email': email_id, 'recovery_email': 'primary',
'displayname': 'A'*200,
'password': 'newpassword'}, follow_redirects=True) 'password': 'newpassword'}, follow_redirects=True)
dump('user_update_invalid_display_name', r) dump('user_update_invalid_display_name', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
user_updated = self.get_user() user_updated = self.get_user()
self.assertEqual(user_updated.displayname, user_unupdated.displayname) self.assertEqual(user_updated.displayname, user_unupdated.displayname)
self.assertEqual(user_updated.mail, user_unupdated.mail) self.assertEqual(user_updated.primary_email.address, user_unupdated.primary_email.address)
self.assertEqual(user_updated.loginname, user_unupdated.loginname) self.assertEqual(user_updated.loginname, user_unupdated.loginname)
self.assertFalse(ldap.test_user_bind(user_updated.dn, 'newpassword')) self.assertFalse(user_updated.password.verify('newpassword'))
self.assertTrue(user_updated.password.verify('userpassword'))
def test_show(self): def test_show(self):
r = self.client.get(path=url_for('user.show', uid=self.get_user().uid), follow_redirects=True) r = self.client.get(path=url_for('user.show', id=self.get_user().id), follow_redirects=True)
dump('user_show', r) dump('user_show', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
def test_show_self(self):
r = self.client.get(path=url_for('user.show', id=self.get_admin().id), follow_redirects=True)
dump('user_show_self', r)
self.assertEqual(r.status_code, 200)
def test_delete(self): def test_delete(self):
r = self.client.get(path=url_for('user.delete', uid=self.get_user().uid), follow_redirects=True) r = self.client.get(path=url_for('user.delete', id=self.get_user().id), follow_redirects=True)
dump('user_delete', r) dump('user_delete', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertIsNone(self.get_user()) self.assertIsNone(self.get_user())
def test_deactivate(self):
r = self.client.get(path=url_for('user.deactivate', id=self.get_user().id), follow_redirects=True)
dump('user_deactivate', r)
self.assertEqual(r.status_code, 200)
self.assertTrue(self.get_user().is_deactivated)
def test_activate(self):
self.get_user().is_deactivated = True
db.session.commit()
r = self.client.get(path=url_for('user.activate', id=self.get_user().id), follow_redirects=True)
dump('user_activate', r)
self.assertEqual(r.status_code, 200)
self.assertFalse(self.get_user().is_deactivated)
def test_disable_mfa(self):
db.session.add(RecoveryCodeMethod(user=self.get_admin()))
user = self.get_user()
for _ in range(10):
db.session.add(RecoveryCodeMethod(user=user))
db.session.add(TOTPMethod(user=self.get_user(), name='My phone'))
db.session.commit()
self.login_as('admin')
admin_methods = len(MFAMethod.query.filter_by(user=self.get_admin()).all())
r = self.client.get(path=url_for('user.disable_mfa', id=self.get_user().id), follow_redirects=True)
dump('user_disable_mfa', r)
self.assertEqual(r.status_code, 200)
self.assertEqual(len(MFAMethod.query.filter_by(user=self.get_user()).all()), 0)
self.assertEqual(len(MFAMethod.query.filter_by(user=self.get_admin()).all()), admin_methods)
def test_csvimport(self): def test_csvimport(self):
role1 = Role(name='role1') role1 = Role(name='role1')
db.session.add(role1) db.session.add(role1)
...@@ -276,154 +419,74 @@ newuser12,newuser12@example.com,{role1.id};{role1.id} ...@@ -276,154 +419,74 @@ newuser12,newuser12@example.com,{role1.id};{role1.id}
r = self.client.post(path=url_for('user.csvimport'), data={'csv': data}, follow_redirects=True) r = self.client.post(path=url_for('user.csvimport'), data={'csv': data}, follow_redirects=True)
dump('user_csvimport', r) dump('user_csvimport', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
user = User.query.get('uid=newuser1,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) user = User.query.filter_by(loginname='newuser1').one_or_none()
self.assertIsNotNone(user) self.assertIsNotNone(user)
self.assertEqual(user.loginname, 'newuser1') self.assertEqual(user.loginname, 'newuser1')
self.assertEqual(user.displayname, 'newuser1') self.assertEqual(user.displayname, 'newuser1')
self.assertEqual(user.mail, 'newuser1@example.com') self.assertEqual(user.primary_email.address, 'newuser1@example.com')
roles = sorted([r.name for r in user.roles]) roles = sorted([r.name for r in user.roles])
self.assertEqual(roles, []) self.assertEqual(roles, [])
user = User.query.get('uid=newuser2,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) user = User.query.filter_by(loginname='newuser2').one_or_none()
self.assertIsNotNone(user) self.assertIsNotNone(user)
self.assertEqual(user.loginname, 'newuser2') self.assertEqual(user.loginname, 'newuser2')
self.assertEqual(user.displayname, 'newuser2') self.assertEqual(user.displayname, 'newuser2')
self.assertEqual(user.mail, 'newuser2@example.com') self.assertEqual(user.primary_email.address, 'newuser2@example.com')
roles = sorted([r.name for r in user.roles]) roles = sorted([r.name for r in user.roles])
self.assertEqual(roles, ['role1']) self.assertEqual(roles, ['role1'])
user = User.query.get('uid=newuser3,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) user = User.query.filter_by(loginname='newuser3').one_or_none()
self.assertIsNotNone(user) self.assertIsNotNone(user)
self.assertEqual(user.loginname, 'newuser3') self.assertEqual(user.loginname, 'newuser3')
self.assertEqual(user.displayname, 'newuser3') self.assertEqual(user.displayname, 'newuser3')
self.assertEqual(user.mail, 'newuser3@example.com') self.assertEqual(user.primary_email.address, 'newuser3@example.com')
roles = sorted([r.name for r in user.roles]) roles = sorted([r.name for r in user.roles])
self.assertEqual(roles, ['role1', 'role2']) self.assertEqual(roles, ['role1', 'role2'])
user = User.query.get('uid=newuser4,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) user = User.query.filter_by(loginname='newuser4').one_or_none()
self.assertIsNotNone(user) self.assertIsNotNone(user)
self.assertEqual(user.loginname, 'newuser4') self.assertEqual(user.loginname, 'newuser4')
self.assertEqual(user.displayname, 'newuser4') self.assertEqual(user.displayname, 'newuser4')
self.assertEqual(user.mail, 'newuser4@example.com') self.assertEqual(user.primary_email.address, 'newuser4@example.com')
roles = sorted([r.name for r in user.roles]) roles = sorted([r.name for r in user.roles])
self.assertEqual(roles, []) self.assertEqual(roles, [])
user = User.query.get('uid=newuser5,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) user = User.query.filter_by(loginname='newuser5').one_or_none()
self.assertIsNotNone(user) self.assertIsNotNone(user)
self.assertEqual(user.loginname, 'newuser5') self.assertEqual(user.loginname, 'newuser5')
self.assertEqual(user.displayname, 'newuser5') self.assertEqual(user.displayname, 'newuser5')
self.assertEqual(user.mail, 'newuser5@example.com') self.assertEqual(user.primary_email.address, 'newuser5@example.com')
roles = sorted([r.name for r in user.roles]) roles = sorted([r.name for r in user.roles])
self.assertEqual(roles, []) self.assertEqual(roles, [])
user = User.query.get('uid=newuser6,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) user = User.query.filter_by(loginname='newuser6').one_or_none()
self.assertIsNotNone(user) self.assertIsNotNone(user)
self.assertEqual(user.loginname, 'newuser6') self.assertEqual(user.loginname, 'newuser6')
self.assertEqual(user.displayname, 'newuser6') self.assertEqual(user.displayname, 'newuser6')
self.assertEqual(user.mail, 'newuser6@example.com') self.assertEqual(user.primary_email.address, 'newuser6@example.com')
roles = sorted([r.name for r in user.roles]) roles = sorted([r.name for r in user.roles])
self.assertEqual(roles, ['role1', 'role2']) self.assertEqual(roles, ['role1', 'role2'])
self.assertIsNone(User.query.get('uid=newuser7,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE']))) self.assertIsNone(User.query.filter_by(loginname='newuser7').one_or_none())
self.assertIsNone(User.query.get('uid=newuser8,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE']))) self.assertIsNone(User.query.filter_by(loginname='newuser8').one_or_none())
self.assertIsNone(User.query.get('uid=newuser9,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE']))) self.assertIsNone(User.query.filter_by(loginname='newuser9').one_or_none())
user = User.query.get('uid=newuser10,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) user = User.query.filter_by(loginname='newuser10').one_or_none()
self.assertIsNotNone(user) self.assertIsNotNone(user)
self.assertEqual(user.loginname, 'newuser10') self.assertEqual(user.loginname, 'newuser10')
self.assertEqual(user.displayname, 'newuser10') self.assertEqual(user.displayname, 'newuser10')
self.assertEqual(user.mail, 'newuser10@example.com') self.assertEqual(user.primary_email.address, 'newuser10@example.com')
roles = sorted([r.name for r in user.roles]) roles = sorted([r.name for r in user.roles])
self.assertEqual(roles, []) self.assertEqual(roles, [])
user = User.query.get('uid=newuser11,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) user = User.query.filter_by(loginname='newuser11').one_or_none()
self.assertIsNotNone(user) self.assertIsNotNone(user)
self.assertEqual(user.loginname, 'newuser11') self.assertEqual(user.loginname, 'newuser11')
self.assertEqual(user.displayname, 'newuser11') self.assertEqual(user.displayname, 'newuser11')
self.assertEqual(user.mail, 'newuser11@example.com') self.assertEqual(user.primary_email.address, 'newuser11@example.com')
# Currently the csv import is not very robust, imho newuser11 should have role1 and role2! # Currently the csv import is not very robust, imho newuser11 should have role1 and role2!
roles = sorted([r.name for r in user.roles]) roles = sorted([r.name for r in user.roles])
self.assertEqual(roles, ['role2']) self.assertEqual(roles, ['role2'])
user = User.query.get('uid=newuser12,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) user = User.query.filter_by(loginname='newuser12').one_or_none()
self.assertIsNotNone(user) self.assertIsNotNone(user)
self.assertEqual(user.loginname, 'newuser12') self.assertEqual(user.loginname, 'newuser12')
self.assertEqual(user.displayname, 'newuser12') self.assertEqual(user.displayname, 'newuser12')
self.assertEqual(user.mail, 'newuser12@example.com') self.assertEqual(user.primary_email.address, 'newuser12@example.com')
roles = sorted([r.name for r in user.roles]) roles = sorted([r.name for r in user.roles])
self.assertEqual(roles, ['role1']) self.assertEqual(roles, ['role1'])
class TestUserViewsOL(TestUserViews):
use_openldap = True
class TestUserViewsOLUserAsAdmin(TestUserViewsOL):
use_userconnection = True
class TestUserViewsOLUserAsUser(UffdTestCase):
use_userconnection = True
use_openldap = True
def setUp(self):
super().setUp()
self.login_as('user')
def test_view_own(self):
r = self.client.get(path=url_for('user.show', uid=self.get_user().uid), follow_redirects=True)
dump('user_view_own', r)
self.assertEqual(r.status_code, 200)
def test_view_others(self):
r = self.client.get(path=url_for('user.show', uid=self.get_admin().uid), follow_redirects=True)
dump('user_view_others', r)
self.assertEqual(r.status_code, 200)
def test_view_index(self):
r = self.client.get(path=url_for('user.index'), follow_redirects=True)
dump('user_index', r)
self.assertEqual(r.status_code, 200)
def test_update_other_user(self):
user_ = self.get_admin()
db.session.add(Role(name='base', is_default=True))
role1 = Role(name='role1')
db.session.add(role1)
role2 = Role(name='role2')
db.session.add(role2)
role2.members.add(user_)
db.session.commit()
role1_id = role1.id
r = self.client.get(path=url_for('user.show', uid=user_.uid), follow_redirects=True)
dump('user_update', r)
self.assertEqual(r.status_code, 200)
r = self.client.post(path=url_for('user.update', uid=user_.uid),
data={'loginname': user_.loginname, 'mail': user_.mail, 'displayname': user_.displayname + "12345",
f'role-{role1_id}': '1', 'password': ''}, follow_redirects=True)
dump('user_update_submit', r)
self.assertEqual(r.status_code, 200)
_user = self.get_admin()
self.assertEqual(_user.displayname, user_.displayname)
self.assertEqual(_user.mail, user_.mail)
self.assertEqual(_user.uid, user_.uid)
self.assertEqual(_user.loginname, user_.loginname)
def test_new(self):
db.session.add(Role(name='base', is_default=True))
role1 = Role(name='role1')
db.session.add(role1)
role2 = Role(name='role2')
db.session.add(role2)
db.session.commit()
role1_id = role1.id
r = self.client.get(path=url_for('user.show'), follow_redirects=True)
dump('user_new', r)
self.assertEqual(r.status_code, 200)
self.assertIsNone(User.query.get('uid=newuser,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])))
r = self.client.post(path=url_for('user.update'),
data={'loginname': 'newuser', 'mail': 'newuser@example.com', 'displayname': 'New User',
f'role-{role1_id}': '1', 'password': 'newpassword'}, follow_redirects=True)
dump('user_new_submit', r)
self.assertEqual(r.status_code, 200)
user = User.query.get('uid=newuser,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE']))
self.assertIsNone(user)
def test_delete(self):
r = self.client.get(path=url_for('user.delete', uid=self.get_admin().uid), follow_redirects=True)
dump('user_delete', r)
self.assertEqual(r.status_code, 200)
self.assertIsNotNone(self.get_admin())
class TestGroupViews(UffdTestCase): class TestGroupViews(UffdTestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
...@@ -439,8 +502,121 @@ class TestGroupViews(UffdTestCase): ...@@ -439,8 +502,121 @@ class TestGroupViews(UffdTestCase):
dump('group_show', r) dump('group_show', r)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
class TestGroupViewsOL(TestGroupViews): def test_new(self):
use_openldap = True r = self.client.get(path=url_for('group.show'), follow_redirects=True)
dump('group_new', r)
self.assertEqual(r.status_code, 200)
self.assertIsNone(Group.query.filter_by(name='newgroup').one_or_none())
r = self.client.post(path=url_for('group.update'),
data={'unix_gid': '', 'name': 'newgroup', 'description': 'Test description'},
follow_redirects=True)
dump('group_new_submit', r)
self.assertEqual(r.status_code, 200)
group = Group.query.filter_by(name='newgroup').one_or_none()
self.assertIsNotNone(group)
self.assertEqual(group.name, 'newgroup')
self.assertEqual(group.description, 'Test description')
self.assertGreaterEqual(group.unix_gid, self.app.config['GROUP_MIN_GID'])
self.assertLessEqual(group.unix_gid, self.app.config['GROUP_MAX_GID'])
def test_new_fixed_gid(self):
gid = self.app.config['GROUP_MAX_GID'] - 1
r = self.client.post(path=url_for('group.update'),
data={'unix_gid': str(gid), 'name': 'newgroup', 'description': 'Test description'},
follow_redirects=True)
dump('group_new_fixed_gid', r)
self.assertEqual(r.status_code, 200)
group = Group.query.filter_by(name='newgroup').one_or_none()
self.assertIsNotNone(group)
self.assertEqual(group.name, 'newgroup')
self.assertEqual(group.description, 'Test description')
self.assertEqual(group.unix_gid, gid)
def test_new_existing_name(self):
gid = self.app.config['GROUP_MAX_GID'] - 1
db.session.add(Group(name='newgroup', description='Original description', unix_gid=gid))
db.session.commit()
r = self.client.post(path=url_for('group.update'),
data={'unix_gid': '', 'name': 'newgroup', 'description': 'New description'},
follow_redirects=True)
dump('group_new_existing_name', r)
self.assertEqual(r.status_code, 400)
group = Group.query.filter_by(name='newgroup').one_or_none()
self.assertIsNotNone(group)
self.assertEqual(group.name, 'newgroup')
self.assertEqual(group.description, 'Original description')
self.assertEqual(group.unix_gid, gid)
def test_new_name_too_long(self):
r = self.client.post(path=url_for('group.update'),
data={'unix_gid': '', 'name': 'a'*33, 'description': 'New description'},
follow_redirects=True)
dump('group_new_name_too_long', r)
self.assertEqual(r.status_code, 400)
group = Group.query.filter_by(name='a'*33).one_or_none()
self.assertIsNone(group)
def test_new_name_too_short(self):
r = self.client.post(path=url_for('group.update'),
data={'unix_gid': '', 'name': '', 'description': 'New description'},
follow_redirects=True)
dump('group_new_name_too_short', r)
self.assertEqual(r.status_code, 400)
group = Group.query.filter_by(name='').one_or_none()
self.assertIsNone(group)
def test_new_name_invalid(self):
r = self.client.post(path=url_for('group.update'),
data={'unix_gid': '', 'name': 'foo bar', 'description': 'New description'},
follow_redirects=True)
dump('group_new_name_invalid', r)
self.assertEqual(r.status_code, 400)
group = Group.query.filter_by(name='foo bar').one_or_none()
self.assertIsNone(group)
def test_new_existing_gid(self):
gid = self.app.config['GROUP_MAX_GID'] - 1
db.session.add(Group(name='newgroup', description='Original description', unix_gid=gid))
db.session.commit()
r = self.client.post(path=url_for('group.update'),
data={'unix_gid': str(gid), 'name': 'newgroup2', 'description': 'New description'},
follow_redirects=True)
dump('group_new_existing_gid', r)
self.assertEqual(r.status_code, 400)
group = Group.query.filter_by(name='newgroup').one_or_none()
self.assertIsNotNone(group)
self.assertEqual(group.name, 'newgroup')
self.assertEqual(group.description, 'Original description')
self.assertEqual(group.unix_gid, gid)
self.assertIsNone(Group.query.filter_by(name='newgroup2').one_or_none())
def test_update(self):
group = Group(name='newgroup', description='Original description')
db.session.add(group)
db.session.commit()
group_id = group.id
group_gid = group.unix_gid
new_gid = self.app.config['GROUP_MAX_GID'] - 1
r = self.client.post(path=url_for('group.update', id=group_id),
data={'unix_gid': str(new_gid), 'name': 'newgroup_changed', 'description': 'New description'},
follow_redirects=True)
dump('group_update', r)
self.assertEqual(r.status_code, 200)
group = Group.query.get(group_id)
self.assertEqual(group.name, 'newgroup') # Not changed
self.assertEqual(group.description, 'New description') # Changed
self.assertEqual(group.unix_gid, group_gid) # Not changed
class TestGroupViewsOLUser(TestGroupViewsOL): def test_delete(self):
use_userconnection = True group1 = Group(name='newgroup1', description='Original description1')
group2 = Group(name='newgroup2', description='Original description2')
db.session.add(group1)
db.session.add(group2)
db.session.commit()
group1_id = group1.id
group2_id = group2.id
r = self.client.get(path=url_for('group.delete', id=group1_id), follow_redirects=True)
dump('group_delete', r)
self.assertEqual(r.status_code, 200)
self.assertIsNone(Group.query.get(group1_id))
self.assertIsNotNone(Group.query.get(group2_id))
...@@ -2,62 +2,74 @@ import os ...@@ -2,62 +2,74 @@ import os
import secrets import secrets
import sys import sys
from flask import Flask, redirect, url_for, request from flask import Flask, redirect, url_for, request, render_template
from flask_babel import Babel from flask_babel import Babel
from werkzeug.routing import IntegerConverter from babel.dates import LOCALTZ
from werkzeug.serving import make_ssl_devcert from werkzeug.exceptions import Forbidden
from werkzeug.contrib.profiler import ProfilerMiddleware
from werkzeug.exceptions import InternalServerError
from flask_migrate import Migrate from flask_migrate import Migrate
import uffd.ldap from .database import db, customize_db_engine
from uffd.database import db, SQLAlchemyJSON from .template_helper import register_template_helper
from uffd.template_helper import register_template_helper from .navbar import setup_navbar
from uffd.navbar import setup_navbar from .csrf import bp as csrf_bp
from uffd.secure_redirect import secure_local_redirect from . import models, views, commands
def load_config_file(app, cfg_name, silent=False): def load_config_file(app, path, silent=False):
cfg_path = os.path.join(app.instance_path, cfg_name) if not os.path.exists(path):
if not os.path.exists(cfg_path):
if not silent: if not silent:
raise Exception(f"Config file {cfg_path} not found") raise Exception(f"Config file {path} not found")
return False return False
if cfg_path.endswith(".json"): if path.endswith(".json"):
app.config.from_json(cfg_path) app.config.from_json(path)
elif cfg_path.endswith(".yaml") or cfg_path.endswith(".yml"): elif path.endswith(".yaml") or path.endswith(".yml"):
import yaml # pylint: disable=import-outside-toplevel disable=import-error import yaml # pylint: disable=import-outside-toplevel disable=import-error
with open(cfg_path, encoding='utf-8') as ymlfile: with open(path, encoding='utf-8') as ymlfile:
data = yaml.safe_load(ymlfile) data = yaml.safe_load(ymlfile)
app.config.from_mapping(data) app.config.from_mapping(data)
else: else:
app.config.from_pyfile(cfg_path, silent=True) app.config.from_pyfile(path, silent=True)
return True return True
def create_app(test_config=None): # pylint: disable=too-many-locals def init_config(app: Flask, test_config):
# create and configure the app
app = Flask(__name__, instance_relative_config=False)
app.json_encoder = SQLAlchemyJSON
# set development default config values # set development default config values
app.config.from_mapping( app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{os.path.join(app.instance_path, 'uffd.sqlit3')}"
SECRET_KEY=secrets.token_hex(128),
SQLALCHEMY_DATABASE_URI="sqlite:///{}".format(os.path.join(app.instance_path, 'uffd.sqlit3')),
)
app.config.from_pyfile('default_config.cfg') app.config.from_pyfile('default_config.cfg')
# load config # load config
if test_config is not None: if test_config is not None:
app.config.from_mapping(test_config) app.config.from_mapping(test_config)
elif os.environ.get("CONFIG_FILENAME"): elif os.environ.get('CONFIG_PATH'):
load_config_file(app, os.environ["CONFIG_FILENAME"], silent=False) load_config_file(app, os.environ['CONFIG_PATH'], silent=False)
else: else:
for cfg_name in ["config.cfg", "config.json", "config.yml", "config.yaml"]: for filename in ["config.cfg", "config.json", "config.yml", "config.yaml"]:
if load_config_file(app, cfg_name, silent=True): if load_config_file(app, os.path.join(app.instance_path, filename), silent=True):
break break
if app.secret_key is None:
if app.env == "production":
raise Exception("SECRET_KEY not configured and we are running in production mode!")
app.secret_key = secrets.token_hex(128)
def create_app(test_config=None): # pylint: disable=too-many-locals,too-many-statements
app = Flask(__name__, instance_relative_config=False)
init_config(app, test_config)
register_template_helper(app) register_template_helper(app)
setup_navbar(app)
# Sort the navbar positions by their blueprint names (from the left)
if app.config['DEFAULT_PAGE_SERVICES']:
positions = ["service", "selfservice"]
else:
positions = ["selfservice", "service"]
positions += ["rolemod", "invite", "user", "group", "role", "mail"]
setup_navbar(app, positions)
app.register_blueprint(csrf_bp)
views.init_app(app)
commands.init_app(app)
# We never want to fail here, but at a file access that doesn't work. # We never want to fail here, but at a file access that doesn't work.
# We might only have read access to app.instance_path # We might only have read access to app.instance_path
...@@ -67,67 +79,31 @@ def create_app(test_config=None): # pylint: disable=too-many-locals ...@@ -67,67 +79,31 @@ def create_app(test_config=None): # pylint: disable=too-many-locals
pass pass
db.init_app(app) db.init_app(app)
Migrate(app, db, render_as_batch=True, directory='uffd/migrations') Migrate(app, db, render_as_batch=True, directory=os.path.join(app.root_path, 'migrations'))
# pylint: disable=C0415 with app.app_context():
from uffd import user, selfservice, role, mail, session, csrf, mfa, oauth2, services, signup, rolemod, invite, api customize_db_engine(db.engine)
# pylint: enable=C0415
for i in user.bp + selfservice.bp + role.bp + mail.bp + session.bp + csrf.bp + mfa.bp + oauth2.bp + services.bp + rolemod.bp + api.bp:
app.register_blueprint(i)
if app.config['LDAP_SERVICE_USER_BIND'] and (app.config['ENABLE_INVITE'] or
app.config['SELF_SIGNUP'] or
app.config['ENABLE_PASSWORDRESET'] or
app.config['ENABLE_ROLESELFSERVICE']):
raise InternalServerError(description="You cannot use INVITES, SIGNUP, PASSWORDRESET or ROLESELFSERVICE when using a USER_BIND!")
if app.config['ENABLE_INVITE'] or app.config['SELF_SIGNUP']:
for i in signup.bp:
app.register_blueprint(i)
if app.config['ENABLE_INVITE']:
for i in invite.bp:
app.register_blueprint(i)
@app.shell_context_processor @app.shell_context_processor
def push_request_context(): #pylint: disable=unused-variable def push_request_context(): #pylint: disable=unused-variable
app.test_request_context().push() # LDAP ORM requires request context ctx = {name: getattr(models, name) for name in models.__all__}
return {'db': db, 'ldap': uffd.ldap.ldap} ctx.setdefault('db', db)
return ctx
@app.route("/")
def index(): #pylint: disable=unused-variable # flask-babel requires pytz-style timezone objects, but in rare cases (e.g.
return redirect(url_for('selfservice.index')) # non-IANA TZ values) LOCALTZ is stdlib-style (without normalize/localize)
if not hasattr(LOCALTZ, 'normalize'):
@app.route('/lang', methods=['POST']) LOCALTZ.normalize = lambda dt: dt
def setlang(): #pylint: disable=unused-variable if not hasattr(LOCALTZ, 'localize'):
resp = secure_local_redirect(request.values.get('ref', '/')) LOCALTZ.localize = lambda dt: dt.replace(tzinfo=LOCALTZ)
if 'lang' in request.values:
resp.set_cookie('language', request.values['lang']) class PatchedBabel(Babel):
return resp @property
def default_timezone(self):
@app.teardown_request if self.app.config['BABEL_DEFAULT_TIMEZONE'] == 'LOCALTZ':
def close_connection(exception): #pylint: disable=unused-variable,unused-argument return LOCALTZ
if hasattr(request, "ldap_connection"): return super().default_timezone
request.ldap_connection.unbind()
babel = PatchedBabel(app, default_timezone='LOCALTZ')
@app.cli.command("gendevcert", help='Generates a self-signed TLS certificate for development')
def gendevcert(): #pylint: disable=unused-variable
if os.path.exists('devcert.crt') or os.path.exists('devcert.key'):
print('Refusing to overwrite existing "devcert.crt"/"devcert.key" file!')
return
make_ssl_devcert('devcert')
print('Certificate written to "devcert.crt", private key to "devcert.key".')
print('Run `flask run --cert devcert.crt --key devcert.key` to use it.')
@app.cli.command("profile", help='Runs app with profiler')
def profile(): #pylint: disable=unused-variable
# app.run() is silently ignored if executed from commands. We really want
# to do this, so we overwrite the check by overwriting the environment
# variable.
os.environ['FLASK_RUN_FROM_CLI'] = 'false'
app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[30])
app.run(debug=True)
babel = Babel(app)
@babel.localeselector @babel.localeselector
def get_locale(): #pylint: disable=unused-variable def get_locale(): #pylint: disable=unused-variable
......
from .views import bp as _bp
bp = [_bp]
import functools
from flask import Blueprint, jsonify, current_app, request, abort
from uffd.user.models import User, Group
from uffd.session.views import login_get_user, login_ratelimit
bp = Blueprint('api', __name__, template_folder='templates', url_prefix='/api/v1/')
def apikey_required(scope=None):
def wrapper(func):
@functools.wraps(func)
def decorator(*args, **kwargs):
if 'Authorization' not in request.headers or not request.headers['Authorization'].startswith('Bearer '):
return 'Unauthorized', 401, {'WWW-Authenticate': 'Bearer'}
token = request.headers['Authorization'][7:].strip()
request.api_client = current_app.config['API_CLIENTS'].get(token)
if request.api_client is None:
return 'Unauthorized', 401, {'WWW-Authenticate': 'Bearer error="invalid_token"'}
if scope is not None and scope not in request.api_client.get('scopes', []):
return 'Unauthorized', 401, {'WWW-Authenticate': 'Bearer error="insufficient_scope",scope="%s"'%scope}
return func(*args, **kwargs)
return decorator
return wrapper
def generate_group_dict(group):
return {'id': group.gid, 'name': group.name,
'members': [user.loginname for user in group.members]}
@bp.route('/getgroups', methods=['GET', 'POST'])
@apikey_required()
def getgroups():
if len(request.values) > 1:
abort(400)
key = (list(request.values.keys()) or [None])[0]
values = request.values.getlist(key)
if key is None:
groups = Group.query.all()
elif key == 'id' and len(values) == 1:
groups = Group.query.filter_by(gid=values[0]).all()
elif key == 'name' and len(values) == 1:
groups = Group.query.filter_by(name=values[0]).all()
elif key == 'member' and len(values) == 1:
user = User.query.filter_by(loginname=values[0]).one_or_none()
groups = [] if user is None else user.groups
else:
abort(400)
return jsonify([generate_group_dict(group) for group in groups])
def generate_user_dict(user, all_groups=None):
if all_groups is None:
all_groups = user.groups
return {'id': user.uid, 'loginname': user.loginname, 'email': user.mail, 'displayname': user.displayname,
'groups': [group.name for group in all_groups if user in group.members]}
@bp.route('/getusers', methods=['GET', 'POST'])
@apikey_required()
def getusers():
if len(request.values) > 1:
abort(400)
key = (list(request.values.keys()) or [None])[0]
values = request.values.getlist(key)
if key is None:
users = User.query.all()
elif key == 'id' and len(values) == 1:
users = User.query.filter_by(uid=values[0]).all()
elif key == 'loginname' and len(values) == 1:
users = User.query.filter_by(loginname=values[0]).all()
elif key == 'email' and len(values) == 1:
users = User.query.filter_by(mail=values[0]).all()
elif key == 'group' and len(values) == 1:
group = Group.query.filter_by(name=values[0]).one_or_none()
users = [] if group is None else group.members
else:
abort(400)
all_groups = None
if len(users) > 1:
all_groups = Group.query.all()
return jsonify([generate_user_dict(user, all_groups) for user in users])
@bp.route('/checkpassword', methods=['POST'])
@apikey_required('checkpassword')
def checkpassword():
if set(request.values.keys()) != {'loginname', 'password'}:
abort(400)
username = request.form['loginname'].lower()
password = request.form['password']
login_delay = login_ratelimit.get_delay(username)
if login_delay:
return 'Too Many Requests', 429, {'Retry-After': '%d'%login_delay}
user = login_get_user(username, password)
if user is None:
login_ratelimit.log(username)
return jsonify(None)
return jsonify(generate_user_dict(user))
[python: **.py] [python: **.py]
[jinja2: **/templates/**.html] [jinja2: **/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_
from .user import user_command
from .group import group_command
from .role import role_command
from .profile import profile_command
from .gendevcert import gendevcert_command
from .cleanup import cleanup_command
from .roles_update_all import roles_update_all_command
from .unique_email_addresses import unique_email_addresses_command
def init_app(app):
app.cli.add_command(user_command)
app.cli.add_command(group_command)
app.cli.add_command(role_command)
app.cli.add_command(gendevcert_command)
app.cli.add_command(profile_command)
app.cli.add_command(cleanup_command)
app.cli.add_command(roles_update_all_command)
app.cli.add_command(unique_email_addresses_command)
import click
from flask.cli import with_appcontext
from uffd.tasks import cleanup_task
from uffd.database import db
@click.command('cleanup', help='Cleanup expired data')
@with_appcontext
def cleanup_command():
cleanup_task.run()
db.session.commit()
import os
import click
from werkzeug.serving import make_ssl_devcert
@click.command("gendevcert", help='Generates a self-signed TLS certificate for development')
def gendevcert_command(): #pylint: disable=unused-variable
if os.path.exists('devcert.crt') or os.path.exists('devcert.key'):
print('Refusing to overwrite existing "devcert.crt"/"devcert.key" file!')
return
make_ssl_devcert('devcert')
print('Certificate written to "devcert.crt", private key to "devcert.key".')
print('Run `flask run --cert devcert.crt --key devcert.key` to use it.')
from flask import current_app
from flask.cli import AppGroup
from sqlalchemy.exc import IntegrityError
import click
from uffd.database import db
from uffd.models import Group
group_command = AppGroup('group', help='Manage groups')
@group_command.command(help='List names of all groups')
def list():
with current_app.test_request_context():
for group in Group.query:
click.echo(group.name)
@group_command.command(help='Show details of group')
@click.argument('name')
def show(name):
with current_app.test_request_context():
group = Group.query.filter_by(name=name).one_or_none()
if group is None:
raise click.ClickException(f'Group {name} not found')
click.echo(f'Name: {group.name}')
click.echo(f'Unix GID: {group.unix_gid}')
click.echo(f'Description: {group.description}')
click.echo(f'Members: {", ".join([user.loginname for user in group.members])}')
@group_command.command(help='Create new group')
@click.argument('name')
@click.option('--description', default='', help='Set description text. Empty per default.')
def create(name, description):
with current_app.test_request_context():
group = Group(description=description)
if not group.set_name(name):
raise click.ClickException('Invalid name')
try:
db.session.add(group)
db.session.commit()
except IntegrityError:
# pylint: disable=raise-missing-from
raise click.ClickException(f'A group with name "{name}" already exists')
@group_command.command(help='Update group attributes')
@click.argument('name')
@click.option('--description', help='Set description text.')
def update(name, description):
with current_app.test_request_context():
group = Group.query.filter_by(name=name).one_or_none()
if group is None:
raise click.ClickException(f'Group {name} not found')
if description is not None:
group.description = description
db.session.commit()
@group_command.command(help='Delete group')
@click.argument('name')
def delete(name):
with current_app.test_request_context():
group = Group.query.filter_by(name=name).one_or_none()
if group is None:
raise click.ClickException(f'Group {name} not found')
db.session.delete(group)
db.session.commit()
import os
from flask import current_app
from flask.cli import with_appcontext
import click
try:
from werkzeug.middleware.profiler import ProfilerMiddleware
except ImportError:
from werkzeug.contrib.profiler import ProfilerMiddleware
@click.command("profile", help='Runs app with profiler')
@with_appcontext
def profile_command(): #pylint: disable=unused-variable
# app.run() is silently ignored if executed from commands. We really want
# to do this, so we overwrite the check by overwriting the environment
# variable.
os.environ['FLASK_RUN_FROM_CLI'] = 'false'
current_app.wsgi_app = ProfilerMiddleware(current_app.wsgi_app, restrictions=[30])
current_app.run(debug=True)
from flask import current_app
from flask.cli import AppGroup
from sqlalchemy.exc import IntegrityError
import click
from uffd.database import db
from uffd.models import Group, Role, RoleGroup
role_command = AppGroup('role', help='Manage roles')
# pylint: disable=too-many-arguments,too-many-locals
def update_attrs(role, description=None, default=None,
moderator_group=None, clear_moderator_group=False,
clear_groups=False, add_group=tuple(), remove_group=tuple(),
clear_roles=False, add_role=tuple(), remove_role=tuple()):
if description is not None:
role.description = description
if default is not None:
role.is_default = default
if clear_moderator_group:
role.moderator_group = None
elif moderator_group is not None:
group = Group.query.filter_by(name=moderator_group).one_or_none()
if group is None:
raise click.ClickException(f'Moderaor group {moderator_group} not found')
role.moderator_group = group
if clear_groups:
role.groups.clear()
for group_name in add_group:
group = Group.query.filter_by(name=group_name).one_or_none()
if group is None:
raise click.ClickException(f'Group {group_name} not found')
role.groups[group] = RoleGroup(group=group)
for group_name in remove_group:
group = Group.query.filter_by(name=group_name).one_or_none()
if group is None:
raise click.ClickException(f'Group {group_name} not found')
del role.groups[group]
if clear_roles:
role.included_roles.clear()
for role_name in add_role:
_role = Role.query.filter_by(name=role_name).one_or_none()
if _role is None:
raise click.ClickException(f'Role {role_name} not found')
role.included_roles.append(_role)
for role_name in remove_role:
_role = Role.query.filter_by(name=role_name).one_or_none()
if _role is None:
raise click.ClickException(f'Role {role_name} not found')
role.included_roles.remove(_role)
@role_command.command(help='List names of all roles')
def list():
with current_app.test_request_context():
for role in Role.query:
click.echo(role.name)
@role_command.command(help='Show details of group')
@click.argument('name')
def show(name):
with current_app.test_request_context():
role = Role.query.filter_by(name=name).one_or_none()
if role is None:
raise click.ClickException(f'Role {name} not found')
click.echo(f'Name: {role.name}')
click.echo(f'Description: {role.description}')
click.echo(f'Default: {role.is_default}')
click.echo(f'Moderator group: {role.moderator_group.name if role.moderator_group else None}')
click.echo(f'Direct groups: {", ".join(sorted([group.name for group in role.groups]))}')
click.echo(f'Effective groups: {", ".join(sorted([group.name for group in role.groups_effective]))}')
click.echo(f'Included roles: {", ".join(sorted([irole.name for irole in role.included_roles]))}')
click.echo(f'Direct members: {", ".join(sorted([user.loginname for user in role.members]))}')
click.echo(f'Effective members: {", ".join(sorted([user.loginname for user in role.members_effective]))}')
@role_command.command(help='Create new role')
@click.argument('name')
@click.option('--description', default='', help='Set description text.')
@click.option('--default/--no-default', default=False, help='Mark role as default or not. Non-service users are auto-added to default roles.')
@click.option('--moderator-group', metavar='GROUP_NAME', help='Set moderator group. No moderator group if unset.')
@click.option('--add-group', multiple=True, metavar='GROUP_NAME', help='Add group granted to role members. Repeat to add multiple groups.')
@click.option('--add-role', multiple=True, metavar='ROLE_NAME', help='Add role to inherit groups from. Repeat to add multiple roles.')
def create(name, description, default, moderator_group, add_group, add_role):
with current_app.test_request_context():
try:
role = Role(name=name)
update_attrs(role, description, default, moderator_group,
add_group=add_group, add_role=add_role)
db.session.add(role)
role.update_member_groups()
db.session.commit()
except IntegrityError:
# pylint: disable=raise-missing-from
raise click.ClickException(f'A role with name "{name}" already exists')
@role_command.command(help='Update role attributes')
@click.argument('name')
@click.option('--description', help='Set description text.')
@click.option('--default/--no-default', default=None, help='Mark role as default or not. Non-service users are auto-added to default roles.')
@click.option('--moderator-group', metavar='GROUP_NAME', help='Set moderator group.')
@click.option('--no-moderator-group', is_flag=True, flag_value=True, default=False, help='Clear moderator group setting.')
@click.option('--clear-groups', is_flag=True, flag_value=True, default=False, help='Remove all groups granted to role members. Executed before --add-group.')
@click.option('--add-group', multiple=True, metavar='GROUP_NAME', help='Add group granted to role members. Repeat to add multiple groups.')
@click.option('--remove-group', multiple=True, metavar='GROUP_NAME', help='Remove group granted to role members. Repeat to remove multiple groups.')
@click.option('--clear-roles', is_flag=True, flag_value=True, default=False, help='Remove all included roles. Executed before --add-role.')
@click.option('--add-role', multiple=True, metavar='ROLE_NAME', help='Add role to inherit groups from. Repeat to add multiple roles.')
@click.option('--remove-role', multiple=True, metavar='ROLE_NAME', help='Remove included role. Repeat to remove multiple roles.')
def update(name, description, default, moderator_group, no_moderator_group,
clear_groups, add_group, remove_group, clear_roles, add_role, remove_role):
with current_app.test_request_context():
role = Role.query.filter_by(name=name).one_or_none()
if role is None:
raise click.ClickException(f'Role {name} not found')
old_members = set(role.members_effective)
update_attrs(role, description, default, moderator_group,
no_moderator_group, clear_groups, add_group, remove_group,
clear_roles, add_role, remove_role)
for user in old_members:
user.update_groups()
role.update_member_groups()
db.session.commit()
@role_command.command(help='Delete role')
@click.argument('name')
def delete(name):
with current_app.test_request_context():
role = Role.query.filter_by(name=name).one_or_none()
if role is None:
raise click.ClickException(f'Role {name} not found')
old_members = set(role.members_effective)
db.session.delete(role)
for user in old_members:
user.update_groups()
db.session.commit()
import sys
from flask import current_app
from flask.cli import with_appcontext
import click
from uffd.database import db
from uffd.models import User
@click.command('roles-update-all', help='Update group memberships for all users based on their roles')
@click.option('--check-only', is_flag=True)
@with_appcontext
def roles_update_all_command(check_only): #pylint: disable=unused-variable
consistent = True
with current_app.test_request_context():
for user in User.query.all():
groups_added, groups_removed = user.update_groups()
if groups_added:
consistent = False
print('Adding groups [%s] to user %s'%(', '.join([group.name for group in groups_added]), user.loginname))
if groups_removed:
consistent = False
print('Removing groups [%s] from user %s'%(', '.join([group.name for group in groups_removed]), user.loginname))
if not check_only:
db.session.commit()
if check_only and not consistent:
print('No changes were made because --check-only is set')
print()
print('Error: Groups are not consistent with roles in database')
sys.exit(1)
import click
from flask.cli import with_appcontext
from sqlalchemy.exc import IntegrityError
from uffd.database import db
from uffd.models import User, UserEmail, FeatureFlag
# pylint completely fails to understand SQLAlchemy's query functions
# pylint: disable=no-member
@click.group('unique-email-addresses', help='Enable/disable e-mail address uniqueness checks')
def unique_email_addresses_command():
pass
@unique_email_addresses_command.command('enable')
@with_appcontext
def enable_unique_email_addresses_command():
if FeatureFlag.unique_email_addresses:
raise click.ClickException('Uniqueness checks for e-mail addresses are already enabled')
query = db.select([UserEmail.address_normalized, UserEmail.user_id])\
.group_by(UserEmail.address_normalized, UserEmail.user_id)\
.having(db.func.count(UserEmail.id.distinct()) > 1)
for address_normalized, user_id in db.session.execute(query).fetchall():
user = User.query.get(user_id)
user_emails = UserEmail.query.filter_by(address_normalized=address_normalized, user_id=user_id)
click.echo(f'User "{user.loginname}" has the same e-mail address multiple times:', err=True)
for user_email in user_emails:
if user_email.verified:
click.echo(f'- {user_email.address}', err=True)
else:
click.echo(f'- {user_email.address} (unverified)', err=True)
click.echo()
query = db.select([UserEmail.address_normalized, UserEmail.address])\
.where(UserEmail.verified)\
.group_by(UserEmail.address_normalized)\
.having(db.func.count(UserEmail.id.distinct()) > 1)
for address_normalized, address in db.session.execute(query).fetchall():
click.echo(f'E-mail address "{address}" is used by multiple users:', err=True)
user_emails = UserEmail.query.filter_by(address_normalized=address_normalized, verified=True)
for user_email in user_emails:
if user_email.address != address:
click.echo(f'- {user_email.user.loginname} ({user_email.address})', err=True)
else:
click.echo(f'- {user_email.user.loginname}', err=True)
click.echo()
try:
FeatureFlag.unique_email_addresses.enable()
except IntegrityError:
# pylint: disable=raise-missing-from
raise click.ClickException('''Some existing e-mail addresses violate uniqueness checks
You need to fix this manually in the admin interface. Then run this command
again to continue.''')
db.session.commit()
click.echo('Uniqueness checks for e-mail addresses enabled')
@unique_email_addresses_command.command('disable')
@with_appcontext
def disable_unique_email_addresses_command():
if not FeatureFlag.unique_email_addresses:
raise click.ClickException('Uniqueness checks for e-mail addresses are already disabled')
click.echo('''Please note that the option to disable email address uniqueness checks will
be remove in uffd v3.
''', err=True)
FeatureFlag.unique_email_addresses.disable()
db.session.commit()
click.echo('Uniqueness checks for e-mail addresses disabled')
from flask import current_app
from flask.cli import AppGroup
from sqlalchemy.exc import IntegrityError
import click
from uffd.database import db
from uffd.models import User, Role
user_command = AppGroup('user', help='Manage users')
# pylint: disable=too-many-arguments
def update_attrs(user, mail=None, displayname=None, password=None,
prompt_password=False, clear_roles=False,
add_role=tuple(), remove_role=tuple(), deactivate=None):
if password is None and prompt_password:
password = click.prompt('Password', hide_input=True, confirmation_prompt='Confirm password')
if mail is not None and not user.set_primary_email_address(mail):
raise click.ClickException('Invalid mail address')
if displayname is not None and not user.set_displayname(displayname):
raise click.ClickException('Invalid displayname')
if password is not None and not user.set_password(password):
raise click.ClickException('Invalid password')
if deactivate is not None:
user.is_deactivated = deactivate
if clear_roles:
user.roles.clear()
for role_name in add_role:
role = Role.query.filter_by(name=role_name).one_or_none()
if role is None:
raise click.ClickException(f'Role {role_name} not found')
role.members.append(user)
for role_name in remove_role:
role = Role.query.filter_by(name=role_name).one_or_none()
if role is None:
raise click.ClickException(f'Role {role_name} not found')
role.members.remove(user)
user.update_groups()
@user_command.command(help='List login names of all users')
def list():
with current_app.test_request_context():
for user in User.query:
click.echo(user.loginname)
@user_command.command(help='Show details of user')
@click.argument('loginname')
def show(loginname):
with current_app.test_request_context():
user = User.query.filter_by(loginname=loginname).one_or_none()
if user is None:
raise click.ClickException(f'User {loginname} not found')
click.echo(f'Loginname: {user.loginname}')
click.echo(f'Deactivated: {user.is_deactivated}')
click.echo(f'Displayname: {user.displayname}')
click.echo(f'Mail: {user.primary_email.address}')
click.echo(f'Service User: {user.is_service_user}')
click.echo(f'Roles: {", ".join([role.name for role in user.roles])}')
click.echo(f'Groups: {", ".join([group.name for group in user.groups])}')
@user_command.command(help='Create new user')
@click.argument('loginname')
@click.option('--mail', required=True, metavar='EMAIL_ADDRESS', help='E-Mail address')
@click.option('--displayname', help='Set display name. Defaults to login name.')
@click.option('--service/--no-service', default=False, help='Create service or regular (default) user. '+\
'Regular users automatically have roles marked as default. '+\
'Service users do not.')
@click.option('--password', help='Password for SSO login. Login disabled if unset.')
@click.option('--prompt-password', is_flag=True, flag_value=True, default=False, help='Read password interactively from terminal.')
@click.option('--add-role', multiple=True, help='Add role to user. Repeat to add multiple roles.', metavar='ROLE_NAME')
@click.option('--deactivate', is_flag=True, flag_value=True, default=None, help='Deactivate account.')
def create(loginname, mail, displayname, service, password, prompt_password, add_role, deactivate):
with current_app.test_request_context():
if displayname is None:
displayname = loginname
user = User(is_service_user=service)
if not user.set_loginname(loginname, ignore_blocklist=True):
raise click.ClickException('Invalid loginname')
try:
db.session.add(user)
update_attrs(user, mail, displayname, password, prompt_password, add_role=add_role, deactivate=deactivate)
db.session.commit()
except IntegrityError:
# pylint: disable=raise-missing-from
raise click.ClickException('Login name or e-mail address is already in use')
@user_command.command(help='Update user attributes and roles')
@click.argument('loginname')
@click.option('--mail', metavar='EMAIL_ADDRESS', help='Set e-mail address.')
@click.option('--displayname', help='Set display name.')
@click.option('--password', help='Set password for SSO login.')
@click.option('--prompt-password', is_flag=True, flag_value=True, default=False, help='Set password by reading it interactivly from terminal.')
@click.option('--clear-roles', is_flag=True, flag_value=True, default=False, help='Remove all roles from user. Executed before --add-role.')
@click.option('--add-role', multiple=True, help='Add role to user. Repeat to add multiple roles.')
@click.option('--remove-role', multiple=True, help='Remove role from user. Repeat to remove multiple roles.')
@click.option('--deactivate/--activate', default=None, help='Deactivate or reactivate account.')
def update(loginname, mail, displayname, password, prompt_password, clear_roles, add_role, remove_role, deactivate):
with current_app.test_request_context():
user = User.query.filter_by(loginname=loginname).one_or_none()
if user is None:
raise click.ClickException(f'User {loginname} not found')
try:
update_attrs(user, mail, displayname, password, prompt_password, clear_roles, add_role, remove_role, deactivate)
db.session.commit()
except IntegrityError:
# pylint: disable=raise-missing-from
raise click.ClickException('E-mail address is already in use')
@user_command.command(help='Delete user')
@click.argument('loginname')
def delete(loginname):
with current_app.test_request_context():
user = User.query.filter_by(loginname=loginname).one_or_none()
if user is None:
raise click.ClickException(f'User {loginname} not found')
db.session.delete(user)
db.session.commit()