diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4663281930afa35a855b5fe1bc8ed4f8f3f1b7ed..ed8b364207d594f6a9a0baee846272a86b77e132 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,20 +13,46 @@ before_script: linter: stage: test script: - - python3 -m pylint --rcfile .pylintrc --output-format=text uffd | tee pylint.txt + - pip3 install pylint-gitlab # this force-updates jinja2 and some other packages! + - python3 -m pylint --exit-zero --rcfile .pylintrc --output-format=pylint_gitlab.GitlabCodeClimateReporter uffd > codeclimate.json + - python3 -m pylint --exit-zero --rcfile .pylintrc --output-format=pylint_gitlab.GitlabPagesHtmlReporter uffd > pylint.html + - python3 -m pylint --rcfile .pylintrc --output-format=text uffd artifacts: - paths: - - pylint.txt + when: always + paths: + - pylint.html + reports: + codequality: codeclimate.json -#unittest: -# stage: test -# script: -# - python3 -m coverage run runTests.py -# - python3 -m coverage report --include "./*" -# - python3 -m coverage report -m --include "./*" > report.txt -# - python3 -m coverage html --include "./*" -# artifacts: -# paths: -# - htmlcov/* -# - .coverage -# - report.txt +unittests: + stage: test + script: + - python3-coverage run --include './*.py' --omit 'tests/*.py' -m pytest --junitxml=report.xml + - python3-coverage report -m + - python3-coverage html + - python3-coverage xml + artifacts: + when: always + paths: + - htmlcov/index.html + - htmlcov + expose_as: 'Coverage Report' + reports: + cobertura: coverage.xml + junit: report.xml + coverage: '/^TOTAL.*\s+(\d+\%)$/' + +html5validator: + stage: test + script: + - rm -rf pages + - mkdir -p pages + - cp -r uffd/static pages/static + - DUMP_PAGES=pages python3 -m unittest discover tests + - sed -i -e 's/href="\/static\//href=".\/static\//g' -e 's/src="\/static\//src=".\/static\//g' pages/*.html + - html5validator --root pages 2>&1 | tee html5validator.log + artifacts: + when: on_failure + paths: + - pages + - html5validator.log diff --git a/ldap_server_entries.json b/ldap_server_entries.json index d52627fd93dcd22df248fe95137eb4c4b29c03b8..a6b5d8c58dd4ce37133a814b7aec70263877f613 100644 --- a/ldap_server_entries.json +++ b/ldap_server_entries.json @@ -4,7 +4,7 @@ "dn": "uid=testuser,ou=users,dc=example,dc=com", "raw": { "cn": [ - "testuser" + "Test User" ], "createTimestamp": [ "20200101000000Z" @@ -25,7 +25,7 @@ "20001" ], "givenName": [ - "testuser" + "Test User" ], "hasSubordinates": [ "FALSE" @@ -77,7 +77,7 @@ "dn": "uid=testadmin,ou=users,dc=example,dc=com", "raw": { "cn": [ - "testadmin" + "Test Admin" ], "createTimestamp": [ "20200101000000Z" @@ -98,7 +98,7 @@ "20001" ], "givenName": [ - "Testadmin" + "Test Admin" ], "hasSubordinates": [ "FALSE" @@ -296,6 +296,52 @@ "uid=testadmin,ou=users,dc=example,dc=com" ] } + }, + { + "dn": "uid=test,ou=postfix,dc=example,dc=com", + "raw": { + "createTimestamp": [ + "20200101000000Z" + ], + "creatorsName": [ + "cn=admin,dc=example,dc=com" + ], + "entryDN": [ + "uid=test,ou=postfix,dc=example,dc=com" + ], + "entryUUID": [ + "926e5273-a545-4dfe-8f20-d1eeaf41d796" + ], + "hasSubordinates": [ + "FALSE" + ], + "mailacceptinggeneralid": [ + "test1@example.com", + "test2@example.com" + ], + "maildrop": [ + "testuser@mail.example.com" + ], + "modifiersName": [ + "cn=admin,dc=example,dc=com" + ], + "modifyTimestamp": [ + "20200101000000Z" + ], + "objectClass": [ + "top", + "postfixVirtual" + ], + "structuralObjectClass": [ + "postfixVirtual" + ], + "subschemaSubentry": [ + "cn=Subschema" + ], + "uid": [ + "test" + ] + } } ] } diff --git a/tests/test_csrf.py b/tests/test_csrf.py new file mode 100644 index 0000000000000000000000000000000000000000..1486beb74b7a5ce827cdae2a1bba168e4a990454 --- /dev/null +++ b/tests/test_csrf.py @@ -0,0 +1,102 @@ +import unittest + +from flask import Flask, Blueprint, session, url_for + +from uffd.csrf import csrf_bp, csrf_protect + +uid_counter = 0 + +class TestCSRF(unittest.TestCase): + unprotected_ep = 'foo' + protected_ep = 'bar' + + def setUp(self): + self.app = Flask(__name__) + self.app.testing = True + self.app.config['SECRET_KEY'] = 'DEBUGKEY' + self.app.register_blueprint(csrf_bp) + + @self.app.route('/', methods=['GET', 'POST']) + def index(): + return 'SUCCESS', 200 + + @self.app.route('/login', methods=['GET', 'POST']) + def login(): + global uid_counter + session['_csrf_token'] = 'secret_csrf_token%d'%uid_counter + uid_counter += 1 + return 'Ok', 200 + + @self.app.route('/logout', methods=['GET', 'POST']) + def logout(): + session.clear() + return 'Ok', 200 + + @self.app.route('/foo', methods=['GET', 'POST']) + def foo(): + return 'SUCCESS', 200 + + @self.app.route('/bar', methods=['GET', 'POST']) + @csrf_protect() + def bar(): + return 'SUCCESS', 200 + + self.bp = Blueprint('bp', __name__) + + @self.bp.route('/foo', methods=['GET', 'POST']) + @csrf_protect(blueprint=self.bp) # This time on .foo and not on .bar! + def foo(): + return 'SUCCESS', 200 + + @self.bp.route('/bar', methods=['GET', 'POST']) + def bar(): + return 'SUCCESS', 200 + + self.app.register_blueprint(self.bp, url_prefix='/bp/') + self.client = self.app.test_client() + self.client.__enter__() + # Just do some request so that we can use url_for + self.client.get(path='/') + + def tearDown(self): + self.client.__exit__(None, None, None) + + def set_token(self): + self.client.get(path='/login') + + def clear_token(self): + self.client.get(path='/logout') + + def test_notoken_unprotected(self): + url = url_for(self.unprotected_ep) + self.assertTrue('csrf' not in url) + self.assertEqual(self.client.get(path=url).data, b'SUCCESS') + + def test_token_unprotected(self): + self.set_token() + self.test_notoken_unprotected() + + def test_notoken_protected(self): + url = url_for(self.protected_ep) + self.assertNotEqual(self.client.get(path=url).data, b'SUCCESS') + + def test_token_protected(self): + self.set_token() + url = url_for(self.protected_ep) + self.assertEqual(self.client.get(path=url).data, b'SUCCESS') + + def test_wrong_token_protected(self): + self.set_token() + url = url_for(self.protected_ep) + self.set_token() + self.assertNotEqual(self.client.get(path=url).data, b'SUCCESS') + + def test_deleted_token_protected(self): + self.set_token() + url = url_for(self.protected_ep) + self.clear_token() + self.assertNotEqual(self.client.get(path=url).data, b'SUCCESS') + +class TestBlueprintCSRF(TestCSRF): + unprotected_ep = 'bp.bar' + protected_ep = 'bp.foo' diff --git a/tests/test_mail.py b/tests/test_mail.py new file mode 100644 index 0000000000000000000000000000000000000000..cbe3c4abe3386fb2efcb5a7ddea922e87bf49f39 --- /dev/null +++ b/tests/test_mail.py @@ -0,0 +1,92 @@ +import datetime +import time + +from flask import url_for, session + +# These imports are required, because otherwise we get circular imports?! +from uffd import ldap, user + +from uffd.ldap import get_conn +from uffd.mail.models import Mail +from uffd import create_app, db + +from utils import dump, UffdTestCase + +def get_mail(): + return Mail.from_ldap_dn('uid=test,ou=postfix,dc=example,dc=com') + +class TestMailViews(UffdTestCase): + def setUp(self): + super().setUp() + self.client.post(path=url_for('session.login'), + data={'loginname': 'testadmin', 'password': 'adminpassword'}, follow_redirects=True) + + def test_index(self): + r = self.client.get(path=url_for('mail.index'), follow_redirects=True) + dump('mail_index', r) + self.assertEqual(r.status_code, 200) + + def test_index_empty(self): + conn = get_conn() + conn.delete(get_mail().dn) + self.assertIsNone(get_mail()) + r = self.client.get(path=url_for('mail.index'), follow_redirects=True) + dump('mail_index_empty', r) + self.assertEqual(r.status_code, 200) + + def test_show(self): + r = self.client.get(path=url_for('mail.show', uid=get_mail().uid), follow_redirects=True) + dump('mail_show', r) + self.assertEqual(r.status_code, 200) + + def test_new(self): + r = self.client.get(path=url_for('mail.show'), follow_redirects=True) + dump('mail_new', r) + self.assertEqual(r.status_code, 200) + + def test_update(self): + m = get_mail() + self.assertIsNotNone(m) + self.assertEqual(m.uid, 'test') + self.assertEqual(sorted(m.receivers), ['test1@example.com', 'test2@example.com']) + self.assertEqual(sorted(m.destinations), ['testuser@mail.example.com']) + r = self.client.post(path=url_for('mail.update', uid=m.uid), + data={'mail-uid': 'test1', 'mail-receivers': 'foo@bar.com\ntest@bar.com', + 'mail-destinations': 'testuser@mail.example.com\ntestadmin@mail.example.com'}, follow_redirects=True) + dump('mail_update', r) + self.assertEqual(r.status_code, 200) + m = get_mail() + self.assertIsNotNone(m) + self.assertEqual(m.uid, 'test') + self.assertEqual(sorted(m.receivers), ['foo@bar.com', 'test@bar.com']) + self.assertEqual(sorted(m.destinations), ['testadmin@mail.example.com', 'testuser@mail.example.com']) + + def test_create(self): + r = self.client.post(path=url_for('mail.update'), + data={'mail-uid': 'test1', 'mail-receivers': 'foo@bar.com\ntest@bar.com', + 'mail-destinations': 'testuser@mail.example.com\ntestadmin@mail.example.com'}, follow_redirects=True) + dump('mail_create', r) + self.assertEqual(r.status_code, 200) + m = Mail.from_ldap_dn('uid=test1,ou=postfix,dc=example,dc=com') + self.assertEqual(m.uid, 'test1') + self.assertEqual(sorted(m.receivers), ['foo@bar.com', 'test@bar.com']) + self.assertEqual(sorted(m.destinations), ['testadmin@mail.example.com', 'testuser@mail.example.com']) + + def test_create_error(self): + r = self.client.post(path=url_for('mail.update'), + data={'mail-uid': 'test', 'mail-receivers': 'foo@bar.com\ntest@bar.com', + 'mail-destinations': 'testuser@mail.example.com\ntestadmin@mail.example.com'}, follow_redirects=True) + dump('mail_create_error', r) + self.assertEqual(r.status_code, 200) + m = get_mail() + self.assertIsNotNone(m) + self.assertEqual(m.uid, 'test') + self.assertEqual(sorted(m.receivers), ['test1@example.com', 'test2@example.com']) + self.assertEqual(sorted(m.destinations), ['testuser@mail.example.com']) + + def test_delete(self): + self.assertIsNotNone(get_mail()) + r = self.client.get(path=url_for('mail.delete', uid=get_mail().uid), follow_redirects=True) + dump('mail_delete', r) + self.assertEqual(r.status_code, 200) + self.assertIsNone(get_mail()) diff --git a/tests/test_mfa.py b/tests/test_mfa.py new file mode 100644 index 0000000000000000000000000000000000000000..b42a20ae11ed9eea103b013f5e4b40ab4fe057b5 --- /dev/null +++ b/tests/test_mfa.py @@ -0,0 +1,425 @@ +import unittest +import datetime +import time + +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.session.views import get_current_user, is_valid_session +from uffd.mfa.models import MFAMethod, MFAType, RecoveryCodeMethod, TOTPMethod, WebauthnMethod, _hotp +from uffd import create_app, db + +from utils import dump, UffdTestCase + +class TestMfaPrimitives(unittest.TestCase): + def test_hotp(self): + self.assertEqual(_hotp(5555555, b'\xae\xa3T\x05\x89\xd6\xb76\xf61r\x92\xcc\xb5WZ\xe6)\x05q'), '458290') + self.assertEqual(_hotp(5555555, b'\xae\xa3T\x05\x89\xd6\xb76\xf61r\x92\xcc\xb5WZ\xe6)\x05q', digits=8), '20458290') + for digits in range(1, 10): + self.assertEqual(len(_hotp(1, b'abcd', digits=digits)), digits) + self.assertEqual(_hotp(1234, b''), '161024') + self.assertEqual(_hotp(0, b'\x04\x8fM\xcc\x7f\x82\x9c$a\x1b\xb3'), '279354') + self.assertEqual(_hotp(2**64-1, b'abcde'), '899292') + +def get_user(): + return User.from_ldap_dn('uid=testuser,ou=users,dc=example,dc=com') + +def get_admin(): + return User.from_ldap_dn('uid=testadmin,ou=users,dc=example,dc=com') + +def get_fido2_test_cred(): + try: + from fido2.ctap2 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 TestMfaMethodModels(UffdTestCase): + def test_common_attributes(self): + method = MFAMethod(user=get_user(), name='testname') + self.assertTrue(method.created <= datetime.datetime.now()) + self.assertEqual(method.name, 'testname') + self.assertEqual(method.user.loginname, 'testuser') + method.user = get_admin() + self.assertEqual(method.user.loginname, 'testadmin') + + def test_recovery_code_method(self): + method = RecoveryCodeMethod(user=get_user()) + db.session.add(method) + db.session.commit() + db.session = db.create_scoped_session() # Ensure the next query does not return the cached method object + _method = RecoveryCodeMethod.query.get(method.id) + self.assertFalse(hasattr(_method, 'code')) + self.assertFalse(_method.verify('')) + self.assertFalse(_method.verify('A'*8)) + self.assertTrue(_method.verify(method.code)) + + def test_totp_method_attributes(self): + method = TOTPMethod(user=get_user(), name='testname') + self.assertEqual(method.name, 'testname') + # Restore method with key parameter + _method = TOTPMethod(user=get_user(), key=method.key, name='testname') + self.assertEqual(_method.name, 'testname') + self.assertEqual(method.raw_key, _method.raw_key) + self.assertEqual(method.issuer, _method.issuer) + self.assertEqual(method.accountname, _method.accountname) + self.assertEqual(method.key_uri, _method.key_uri) + db.session.add(method) + db.session.commit() + db.session = db.create_scoped_session() # Ensure the next query does not return the cached method object + # Restore method from db + _method = TOTPMethod.query.get(method.id) + self.assertEqual(_method.name, 'testname') + self.assertEqual(method.raw_key, _method.raw_key) + self.assertEqual(method.issuer, _method.issuer) + self.assertEqual(method.accountname, _method.accountname) + self.assertEqual(method.key_uri, _method.key_uri) + + def test_totp_method_verify(self): + method = TOTPMethod(user=get_user()) + counter = int(time.time()/30) + self.assertFalse(method.verify('')) + self.assertFalse(method.verify(_hotp(counter-2, method.raw_key))) + self.assertTrue(method.verify(_hotp(counter, method.raw_key))) + self.assertFalse(method.verify(_hotp(counter+2, method.raw_key))) + + def test_webauthn_method(self): + data = get_fido2_test_cred() + method = WebauthnMethod(user=get_user(), cred=data, name='testname') + self.assertEqual(method.name, 'testname') + db.session.add(method) + db.session.commit() + db.session = db.create_scoped_session() # Ensure the next query does not return the cached method object + _method = WebauthnMethod.query.get(method.id) + self.assertEqual(_method.name, 'testname') + self.assertEqual(bytes(method.cred), bytes(_method.cred)) + self.assertEqual(data.credential_id, _method.cred.credential_id) + self.assertEqual(data.public_key, _method.cred.public_key) + # We only test (de-)serialization here, as everything else is currently implemented in the views + +class TestMfaViews(UffdTestCase): + def setUp(self): + super().setUp() + db.session.add(RecoveryCodeMethod(user=get_admin())) + db.session.add(TOTPMethod(user=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_admin(), cred=get_fido2_test_cred(), name='Admin FIDO2 dongle')) + db.session.commit() + + def login(self): + self.client.post(path=url_for('session.login'), + data={'loginname': 'testuser', 'password': 'userpassword'}, follow_redirects=True) + + def add_recovery_codes(self, count=10): + user = get_user() + for _ in range(count): + db.session.add(RecoveryCodeMethod(user=user)) + db.session.commit() + + def add_totp(self): + db.session.add(TOTPMethod(user=get_user(), name='My phone')) + db.session.commit() + + def add_webauthn(self): + db.session.add(WebauthnMethod(user=get_user(), cred=get_fido2_test_cred(), name='My FIDO2 dongle')) + db.session.commit() + + def test_setup_disabled(self): + self.login() + r = self.client.get(path=url_for('mfa.setup'), follow_redirects=True) + dump('mfa_setup_disabled', r) + self.assertEqual(r.status_code, 200) + + def test_setup_recovery_codes(self): + self.login() + self.add_recovery_codes() + r = self.client.get(path=url_for('mfa.setup'), follow_redirects=True) + dump('mfa_setup_only_recovery_codes', r) + self.assertEqual(r.status_code, 200) + + def test_setup_enabled(self): + self.login() + self.add_recovery_codes() + self.add_totp() + self.add_webauthn() + r = self.client.get(path=url_for('mfa.setup'), follow_redirects=True) + dump('mfa_setup_enabled', r) + self.assertEqual(r.status_code, 200) + + def test_setup_few_recovery_codes(self): + self.login() + self.add_totp() + self.add_recovery_codes(1) + r = self.client.get(path=url_for('mfa.setup'), 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() + self.add_totp() + r = self.client.get(path=url_for('mfa.setup'), follow_redirects=True) + dump('mfa_setup_no_recovery_codes', r) + self.assertEqual(r.status_code, 200) + + def test_disable(self): + self.login() + self.add_recovery_codes() + self.add_totp() + admin_methods = len(MFAMethod.query.filter_by(dn=get_admin().dn).all()) + r = self.client.get(path=url_for('mfa.disable'), follow_redirects=True) + dump('mfa_disable', r) + self.assertEqual(r.status_code, 200) + r = self.client.post(path=url_for('mfa.disable_confirm'), follow_redirects=True) + dump('mfa_disable_submit', r) + self.assertEqual(r.status_code, 200) + self.assertEqual(len(MFAMethod.query.filter_by(dn=get_current_user().dn).all()), 0) + self.assertEqual(len(MFAMethod.query.filter_by(dn=get_admin().dn).all()), admin_methods) + + def test_disable_recovery_only(self): + self.login() + self.add_recovery_codes() + admin_methods = len(MFAMethod.query.filter_by(dn=get_admin().dn).all()) + self.assertNotEqual(len(MFAMethod.query.filter_by(dn=get_current_user().dn).all()), 0) + r = self.client.get(path=url_for('mfa.disable'), follow_redirects=True) + dump('mfa_disable_recovery_only', r) + self.assertEqual(r.status_code, 200) + r = self.client.post(path=url_for('mfa.disable_confirm'), follow_redirects=True) + dump('mfa_disable_recovery_only_submit', r) + self.assertEqual(r.status_code, 200) + self.assertEqual(len(MFAMethod.query.filter_by(dn=get_current_user().dn).all()), 0) + self.assertEqual(len(MFAMethod.query.filter_by(dn=get_admin().dn).all()), admin_methods) + + def test_admin_disable(self): + for method in MFAMethod.query.filter_by(dn=get_admin().dn).all(): + if not isinstance(method, RecoveryCodeMethod): + db.session.delete(method) + db.session.commit() + self.add_recovery_codes() + self.add_totp() + self.client.post(path=url_for('session.login'), + data={'loginname': 'testadmin', 'password': 'adminpassword'}, follow_redirects=True) + self.assertTrue(is_valid_session()) + admin_methods = len(MFAMethod.query.filter_by(dn=get_admin().dn).all()) + r = self.client.get(path=url_for('mfa.admin_disable', uid=get_user().uid), follow_redirects=True) + dump('mfa_admin_disable', r) + self.assertEqual(r.status_code, 200) + self.assertEqual(len(MFAMethod.query.filter_by(dn=get_user().dn).all()), 0) + self.assertEqual(len(MFAMethod.query.filter_by(dn=get_admin().dn).all()), admin_methods) + + def test_setup_recovery(self): + self.login() + self.assertEqual(len(RecoveryCodeMethod.query.filter_by(dn=get_current_user().dn).all()), 0) + r = self.client.post(path=url_for('mfa.setup_recovery'), follow_redirects=True) + dump('mfa_setup_recovery', r) + self.assertEqual(r.status_code, 200) + methods = RecoveryCodeMethod.query.filter_by(dn=get_current_user().dn).all() + self.assertNotEqual(len(methods), 0) + r = self.client.post(path=url_for('mfa.setup_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() + self.add_recovery_codes() + r = self.client.get(path=url_for('mfa.setup_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() + r = self.client.get(path=url_for('mfa.setup_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): + self.login() + self.add_recovery_codes() + self.assertEqual(len(TOTPMethod.query.filter_by(dn=get_current_user().dn).all()), 0) + r = self.client.get(path=url_for('mfa.setup_totp', name='My TOTP Authenticator'), follow_redirects=True) + method = TOTPMethod(get_current_user(), key=session.get('mfa_totp_key', '')) + code = _hotp(int(time.time()/30), method.raw_key) + r = self.client.post(path=url_for('mfa.setup_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(dn=get_current_user().dn).all()), 1) + + def test_setup_totp_finish_without_recovery(self): + self.login() + self.assertEqual(len(TOTPMethod.query.filter_by(dn=get_current_user().dn).all()), 0) + r = self.client.get(path=url_for('mfa.setup_totp', name='My TOTP Authenticator'), follow_redirects=True) + method = TOTPMethod(get_current_user(), key=session.get('mfa_totp_key', '')) + code = _hotp(int(time.time()/30), method.raw_key) + r = self.client.post(path=url_for('mfa.setup_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(dn=get_current_user().dn).all()), 0) + + def test_setup_totp_finish_wrong_code(self): + self.login() + self.add_recovery_codes() + self.assertEqual(len(TOTPMethod.query.filter_by(dn=get_current_user().dn).all()), 0) + r = self.client.get(path=url_for('mfa.setup_totp', name='My TOTP Authenticator'), follow_redirects=True) + method = TOTPMethod(get_current_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('mfa.setup_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) + self.assertEqual(len(TOTPMethod.query.filter_by(dn=get_current_user().dn).all()), 0) + + def test_setup_totp_finish_empty_code(self): + self.login() + self.add_recovery_codes() + self.assertEqual(len(TOTPMethod.query.filter_by(dn=get_current_user().dn).all()), 0) + r = self.client.get(path=url_for('mfa.setup_totp', name='My TOTP Authenticator'), follow_redirects=True) + r = self.client.post(path=url_for('mfa.setup_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) + self.assertEqual(len(TOTPMethod.query.filter_by(dn=get_current_user().dn).all()), 0) + + def test_delete_totp(self): + self.login() + self.add_recovery_codes() + self.add_totp() + method = TOTPMethod(get_current_user(), name='test') + db.session.add(method) + db.session.commit() + self.assertEqual(len(TOTPMethod.query.filter_by(dn=get_current_user().dn).all()), 2) + r = self.client.get(path=url_for('mfa.delete_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(dn=get_current_user().dn).all()), 1) + + # TODO: webauthn setup tests + + def test_auth_integration(self): + self.add_recovery_codes() + self.add_totp() + db.session.commit() + self.assertFalse(is_valid_session()) + r = self.client.post(path=url_for('session.login'), + data={'loginname': 'testuser', 'password': 'userpassword'}, follow_redirects=True) + dump('mfa_auth_redirected', r) + self.assertEqual(r.status_code, 200) + self.assertIn(b'/mfa/auth', r.data) + self.assertFalse(is_valid_session()) + r = self.client.get(path=url_for('mfa.auth'), follow_redirects=False) + dump('mfa_auth', r) + self.assertEqual(r.status_code, 200) + self.assertFalse(is_valid_session()) + + def test_auth_disabled(self): + self.assertFalse(is_valid_session()) + r = self.client.post(path=url_for('session.login'), + data={'loginname': 'testuser', 'password': 'userpassword'}, follow_redirects=False) + r = self.client.get(path=url_for('mfa.auth', ref='/redirecttarget'), follow_redirects=False) + self.assertEqual(r.status_code, 302) + self.assertTrue(r.location.endswith('/redirecttarget')) + self.assertTrue(is_valid_session()) + + def test_auth_recovery_only(self): + self.add_recovery_codes() + self.assertFalse(is_valid_session()) + r = self.client.post(path=url_for('session.login'), + data={'loginname': 'testuser', 'password': 'userpassword'}, follow_redirects=False) + r = self.client.get(path=url_for('mfa.auth', ref='/redirecttarget'), follow_redirects=False) + self.assertEqual(r.status_code, 302) + self.assertTrue(r.location.endswith('/redirecttarget')) + self.assertTrue(is_valid_session()) + + def test_auth_recovery_code(self): + self.add_recovery_codes() + self.add_totp() + method = RecoveryCodeMethod(user=get_user()) + db.session.add(method) + db.session.commit() + method_id = method.id + self.login() + r = self.client.get(path=url_for('mfa.auth'), follow_redirects=False) + dump('mfa_auth_recovery_code', r) + self.assertEqual(r.status_code, 200) + self.assertFalse(is_valid_session()) + r = self.client.post(path=url_for('mfa.auth_finish', ref='/redirecttarget'), data={'code': method.code}) + self.assertEqual(r.status_code, 302) + self.assertTrue(r.location.endswith('/redirecttarget')) + self.assertTrue(is_valid_session()) + 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=get_user(), name='testname') + raw_key = method.raw_key + db.session.add(method) + db.session.commit() + self.login() + r = self.client.get(path=url_for('mfa.auth'), follow_redirects=False) + dump('mfa_auth_totp_code', r) + self.assertEqual(r.status_code, 200) + self.assertFalse(is_valid_session()) + code = _hotp(int(time.time()/30), raw_key) + r = self.client.post(path=url_for('mfa.auth_finish'), data={'code': code}, follow_redirects=True) + dump('mfa_auth_totp_code_submit', r) + self.assertEqual(r.status_code, 200) + self.assertTrue(is_valid_session()) + + def test_auth_empty_code(self): + self.add_recovery_codes() + self.add_totp() + self.login() + r = self.client.get(path=url_for('mfa.auth'), follow_redirects=False) + self.assertEqual(r.status_code, 200) + self.assertFalse(is_valid_session()) + r = self.client.post(path=url_for('mfa.auth_finish'), data={'code': ''}, follow_redirects=True) + dump('mfa_auth_empty_code', r) + self.assertEqual(r.status_code, 200) + self.assertFalse(is_valid_session()) + + def test_auth_invalid_code(self): + self.add_recovery_codes() + self.add_totp() + method = TOTPMethod(user=get_user(), name='testname') + raw_key = method.raw_key + db.session.add(method) + db.session.commit() + self.login() + r = self.client.get(path=url_for('mfa.auth'), follow_redirects=False) + self.assertEqual(r.status_code, 200) + self.assertFalse(is_valid_session()) + code = _hotp(int(time.time()/30), raw_key) + code = str(int(code[0])+1)[-1] + code[1:] + r = self.client.post(path=url_for('mfa.auth_finish'), data={'code': code}, follow_redirects=True) + dump('mfa_auth_invalid_code', r) + self.assertEqual(r.status_code, 200) + self.assertFalse(is_valid_session()) + + @unittest.skip('Not implemented, see #10') + def test_auth_ratelimit(self): + self.add_recovery_codes() + self.add_totp() + method = TOTPMethod(user=get_user(), name='testname') + raw_key = method.raw_key + db.session.add(method) + db.session.commit() + self.login() + self.assertFalse(is_valid_session()) + 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('mfa.auth_finish'), data={'code': inv_code}, follow_redirects=True) + self.assertEqual(r.status_code, 200) + self.assertFalse(is_valid_session()) + r = self.client.post(path=url_for('mfa.auth_finish'), data={'code': code}, follow_redirects=True) + dump('mfa_auth_ratelimit', r) + self.assertEqual(r.status_code, 200) + self.assertFalse(is_valid_session()) + + # TODO: webauthn auth tests diff --git a/tests/test_oauth2.py b/tests/test_oauth2.py new file mode 100644 index 0000000000000000000000000000000000000000..c5d024eca7f63b9fb58538836d99bba8efcf66f9 --- /dev/null +++ b/tests/test_oauth2.py @@ -0,0 +1,110 @@ +import datetime +from urllib.parse import urlparse, parse_qs + +from flask import url_for + +# These imports are required, because otherwise we get circular imports?! +from uffd import ldap, user + +from uffd.session.views import get_current_user +from uffd.user.models import User +from uffd.oauth2.models import OAuth2Client +from uffd import create_app, db, ldap + +from utils import dump, UffdTestCase + +def get_user(): + return User.from_ldap_dn('uid=testuser,ou=users,dc=example,dc=com') + +def get_admin(): + return User.from_ldap_dn('uid=testadmin,ou=users,dc=example,dc=com') + +class TestOAuth2Client(UffdTestCase): + def setUpApp(self): + self.app.config['OAUTH2_CLIENTS'] = { + 'test': {'client_secret': 'testsecret', 'redirect_uris': ['http://localhost:5009/callback', 'http://localhost:5009/callback2']}, + 'test1': {'client_secret': 'testsecret1', 'redirect_uris': ['http://localhost:5008/callback'], 'required_group': 'users'}, + } + + def test_from_id(self): + client = OAuth2Client.from_id('test') + self.assertEqual(client.client_id, 'test') + self.assertEqual(client.client_secret, 'testsecret') + self.assertEqual(client.redirect_uris, ['http://localhost:5009/callback', 'http://localhost:5009/callback2']) + self.assertEqual(client.default_redirect_uri, 'http://localhost:5009/callback') + self.assertEqual(client.default_scopes, ['profile']) + self.assertEqual(client.client_type, 'confidential') + client = OAuth2Client.from_id('test1') + self.assertEqual(client.client_id, 'test1') + self.assertEqual(client.required_group, 'users') + + def test_access_allowed(self): + user = get_user() # has 'users' and 'uffd_access' group + admin = get_admin() # has 'users', 'uffd_access' and 'uffd_admin' group + client = OAuth2Client('test', '', [''], None) + self.assertTrue(client.access_allowed(user)) + self.assertTrue(client.access_allowed(admin)) + client = OAuth2Client('test', '', [''], 'users') + self.assertTrue(client.access_allowed(user)) + self.assertTrue(client.access_allowed(admin)) + client = OAuth2Client('test', '', [''], 'notagroup') + self.assertFalse(client.access_allowed(user)) + self.assertFalse(client.access_allowed(admin)) + client = OAuth2Client('test', '', [''], 'uffd_admin') + self.assertFalse(client.access_allowed(user)) + self.assertTrue(client.access_allowed(admin)) + client = OAuth2Client('test', '', [''], ['uffd_admin']) + self.assertFalse(client.access_allowed(user)) + self.assertTrue(client.access_allowed(admin)) + client = OAuth2Client('test', '', [''], ['uffd_admin', 'notagroup']) + self.assertFalse(client.access_allowed(user)) + self.assertTrue(client.access_allowed(admin)) + client = OAuth2Client('test', '', [''], ['notagroup', 'uffd_admin' ]) + self.assertFalse(client.access_allowed(user)) + self.assertTrue(client.access_allowed(admin)) + client = OAuth2Client('test', '', [''], ['uffd_admin', 'users']) + self.assertTrue(client.access_allowed(user)) + self.assertTrue(client.access_allowed(admin)) + client = OAuth2Client('test', '', [''], ['uffd_admin', 'users']) + self.assertTrue(client.access_allowed(user)) + self.assertTrue(client.access_allowed(admin)) + client = OAuth2Client('test', '', [''], [['uffd_admin', 'users'], ['users', 'uffd_access']]) + self.assertTrue(client.access_allowed(user)) + self.assertTrue(client.access_allowed(admin)) + client = OAuth2Client('test', '', [''], ['uffd_admin', ['users', 'notagroup']]) + self.assertFalse(client.access_allowed(user)) + self.assertTrue(client.access_allowed(admin)) + +class TestViews(UffdTestCase): + def setUpApp(self): + self.app.config['OAUTH2_CLIENTS'] = { + 'test': {'client_secret': 'testsecret', 'redirect_uris': ['http://localhost:5009/callback', 'http://localhost:5009/callback2']}, + 'test1': {'client_secret': 'testsecret1', 'redirect_uris': ['http://localhost:5008/callback'], 'required_group': 'uffd_admin'}, + } + + def test_authorization(self): + self.client.post(path=url_for('session.login'), + data={'loginname': 'testuser', 'password': 'userpassword'}, follow_redirects=True) + state = 'teststate' + r = self.client.get(path=url_for('oauth2.authorize', response_type='code', client_id='test', state=state, redirect_uri='http://localhost:5009/callback'), follow_redirects=False) + self.assertEqual(r.status_code, 302) + self.assertTrue(r.location.startswith('http://localhost:5009/callback')) + args = parse_qs(urlparse(r.location).query) + self.assertEqual(args['state'], [state]) + code = args['code'][0] + r = self.client.post(path=url_for('oauth2.token'), + data={'grant_type': 'authorization_code', 'code': code, 'redirect_uri': 'http://localhost:5009/callback', 'client_id': 'test', 'client_secret': 'testsecret'}, follow_redirects=True) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.content_type, 'application/json') + self.assertEqual(r.json['token_type'], 'Bearer') + self.assertEqual(r.json['scope'], 'profile') + token = r.json['access_token'] + r = self.client.get(path=url_for('oauth2.userinfo'), headers=[('Authorization', 'Bearer %s'%token)], follow_redirects=True) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.content_type, 'application/json') + user = get_user() + self.assertEqual(r.json['id'], user.uid) + self.assertEqual(r.json['name'], user.displayname) + self.assertEqual(r.json['nickname'], user.loginname) + self.assertEqual(r.json['email'], user.mail) + self.assertTrue(r.json.get('groups')) diff --git a/tests/test_role.py b/tests/test_role.py new file mode 100644 index 0000000000000000000000000000000000000000..097a54bf3d60de0380d612a63cf130118f9b83ce --- /dev/null +++ b/tests/test_role.py @@ -0,0 +1,93 @@ +import datetime +import time + +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 Group +from uffd.role.models import Role +from uffd import create_app, db + +from utils import dump, UffdTestCase + +class TestRoleViews(UffdTestCase): + def setUp(self): + super().setUp() + self.client.post(path=url_for('session.login'), + data={'loginname': 'testadmin', 'password': 'adminpassword'}, follow_redirects=True) + + def test_index(self): + db.session.add(Role('base', 'Base role description')) + db.session.add(Role('test1', '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('base', '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.show'), follow_redirects=True) + dump('role_new', r) + self.assertEqual(r.status_code, 200) + + def test_update(self): + role = Role('base', 'Base role description') + db.session.add(role) + db.session.commit() + role.add_group(Group.from_ldap_dn('cn=uffd_admin,ou=groups,dc=example,dc=com')) + db.session.commit() + self.assertEqual(role.name, 'base') + self.assertEqual(role.description, 'Base role description') + self.assertEqual(role.group_dns(), ['cn=uffd_admin,ou=groups,dc=example,dc=com']) + r = self.client.post(path=url_for('role.update', roleid=role.id), + data={'name': 'base1', 'description': 'Base role description1', 'group-20001': '1', 'group-20002': '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.assertEqual(sorted(role.group_dns()), ['cn=uffd_access,ou=groups,dc=example,dc=com', + 'cn=users,ou=groups,dc=example,dc=com']) + # TODO: verify that group memberships are updated (currently not possible with ldap mock!) + + 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', 'group-20001': '1', 'group-20002': '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.assertEqual(sorted(role.group_dns()), ['cn=uffd_access,ou=groups,dc=example,dc=com', + 'cn=users,ou=groups,dc=example,dc=com']) + # TODO: verify that group memberships are updated (currently not possible with ldap mock!) + + def test_delete(self): + role = Role('base', '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!) diff --git a/tests/test_selfservice.py b/tests/test_selfservice.py new file mode 100644 index 0000000000000000000000000000000000000000..3c5800f9615e2938ed404947d5a28a965bd4783c --- /dev/null +++ b/tests/test_selfservice.py @@ -0,0 +1,258 @@ +import datetime +import unittest + +from flask import url_for + +# These imports are required, because otherwise we get circular imports?! +from uffd import ldap, user + +from uffd.session.views import get_current_user +from uffd.selfservice.models import MailToken, PasswordToken +from uffd.user.models import User +from uffd import create_app, db, ldap + +from utils import dump, UffdTestCase + +def get_ldap_password(): + conn = ldap.get_conn() + conn.search('uid=testuser,ou=users,dc=example,dc=com', '(objectClass=person)') + return conn.entries[0]['userPassword'] + +class TestSelfservice(UffdTestCase): + def setUpApp(self): + self.app.config['MAIL_SKIP_SEND'] = True + + def login(self): + self.client.post(path=url_for('session.login'), + data={'loginname': 'testuser', 'password': 'userpassword'}, follow_redirects=True) + + def test_index(self): + self.login() + r = self.client.get(path=url_for('selfservice.index')) + dump('selfservice_index', r) + self.assertEqual(r.status_code, 200) + user = get_current_user() + self.assertIn(user.displayname.encode(), r.data) + self.assertIn(user.loginname.encode(), r.data) + self.assertIn(user.mail.encode(), r.data) + + def test_update_displayname(self): + self.login() + user = get_current_user() + r = self.client.post(path=url_for('selfservice.update'), + data={'displayname': 'New Display Name', 'mail': user.mail, 'password': '', 'password1': ''}, + follow_redirects=True) + dump('update_displayname', r) + self.assertEqual(r.status_code, 200) + _user = get_current_user() + self.assertEqual(_user.displayname, 'New Display Name') + + def test_update_displayname_invalid(self): + self.login() + user = get_current_user() + r = self.client.post(path=url_for('selfservice.update'), + data={'displayname': '', 'mail': user.mail, 'password': '', 'password1': ''}, + follow_redirects=True) + dump('update_displayname_invalid', r) + self.assertEqual(r.status_code, 200) + _user = get_current_user() + self.assertNotEqual(_user.displayname, '') + + def test_update_mail(self): + self.login() + user = get_current_user() + r = self.client.post(path=url_for('selfservice.update'), + data={'displayname': user.displayname, 'mail': 'newemail@example.com', 'password': '', 'password1': ''}, + follow_redirects=True) + dump('update_mail', r) + self.assertEqual(r.status_code, 200) + _user = get_current_user() + self.assertNotEqual(_user.mail, 'newemail@example.com') + token = MailToken.query.filter(MailToken.loginname == user.loginname).first() + self.assertEqual(token.newmail, 'newemail@example.com') + self.assertIn(token.token, str(self.app.last_mail.get_content())) + r = self.client.get(path=url_for('selfservice.token_mail', token=token.token), follow_redirects=True) + self.assertEqual(r.status_code, 200) + _user = get_current_user() + self.assertEqual(_user.mail, 'newemail@example.com') + + def test_update_mail_sendfailure(self): + self.app.config['MAIL_SKIP_SEND'] = 'fail' + self.login() + user = get_current_user() + r = self.client.post(path=url_for('selfservice.update'), + data={'displayname': user.displayname, 'mail': 'newemail@example.com', 'password': '', 'password1': ''}, + follow_redirects=True) + dump('update_mail_sendfailure', r) + self.assertEqual(r.status_code, 200) + _user = get_current_user() + self.assertNotEqual(_user.mail, 'newemail@example.com') + # Maybe also check that there is no new token in the db + + def test_token_mail_emptydb(self): + self.login() + user = get_current_user() + r = self.client.get(path=url_for('selfservice.token_mail', token='A'*128), follow_redirects=True) + dump('token_mail_emptydb', r) + self.assertEqual(r.status_code, 200) + _user = get_current_user() + self.assertEqual(_user.mail, user.mail) + + def test_token_mail_invalid(self): + self.login() + user = get_current_user() + db.session.add(MailToken(loginname=user.loginname, newmail='newusermail@example.com')) + db.session.commit() + r = self.client.get(path=url_for('selfservice.token_mail', token='A'*128), follow_redirects=True) + dump('token_mail_invalid', r) + self.assertEqual(r.status_code, 200) + _user = get_current_user() + self.assertEqual(_user.mail, user.mail) + + @unittest.skip('See #26') + def test_token_mail_wrong_user(self): + self.login() + user = get_current_user() + admin_user = User.from_ldap_dn('uid=testadmin,ou=users,dc=example,dc=com') + db.session.add(MailToken(loginname=user.loginname, newmail='newusermail@example.com')) + admin_token = MailToken(loginname='testadmin', newmail='newadminmail@example.com') + db.session.add(admin_token) + db.session.commit() + r = self.client.get(path=url_for('selfservice.token_mail', token=admin_token.token), follow_redirects=True) + dump('token_mail_wrong_user', r) + self.assertEqual(r.status_code, 200) + _user = get_current_user() + _admin_user = User.from_ldap_dn('uid=testadmin,ou=users,dc=example,dc=com') + self.assertEqual(_user.mail, user.mail) + self.assertEqual(_admin_user.mail, admin_user.mail) + + def test_token_mail_expired(self): + self.login() + user = get_current_user() + token = MailToken(loginname=user.loginname, newmail='newusermail@example.com', + created=(datetime.datetime.now() - datetime.timedelta(days=10))) + db.session.add(token) + db.session.commit() + r = self.client.get(path=url_for('selfservice.token_mail', token=token.token), follow_redirects=True) + dump('token_mail_expired', r) + self.assertEqual(r.status_code, 200) + _user = get_current_user() + self.assertEqual(_user.mail, user.mail) + tokens = MailToken.query.filter(MailToken.loginname == user.loginname).all() + self.assertEqual(len(tokens), 0) + + def test_forgot_password(self): + user = User.from_ldap_dn('uid=testuser,ou=users,dc=example,dc=com') + r = self.client.get(path=url_for('selfservice.forgot_password')) + dump('forgot_password', r) + self.assertEqual(r.status_code, 200) + r = self.client.post(path=url_for('selfservice.forgot_password'), + data={'loginname': user.loginname, 'mail': user.mail}, follow_redirects=True) + dump('forgot_password_submit', r) + self.assertEqual(r.status_code, 200) + token = PasswordToken.query.filter(PasswordToken.loginname == user.loginname).first() + self.assertIsNotNone(token) + self.assertIn(token.token, str(self.app.last_mail.get_content())) + + def test_forgot_password_wrong_user(self): + user = User.from_ldap_dn('uid=testuser,ou=users,dc=example,dc=com') + r = self.client.get(path=url_for('selfservice.forgot_password')) + self.assertEqual(r.status_code, 200) + r = self.client.post(path=url_for('selfservice.forgot_password'), + data={'loginname': 'not_a_user', 'mail': user.mail}, 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 = User.from_ldap_dn('uid=testuser,ou=users,dc=example,dc=com') + 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) + + def test_token_password(self): + user = User.from_ldap_dn('uid=testuser,ou=users,dc=example,dc=com') + oldpw = get_ldap_password() + token = PasswordToken(loginname=user.loginname) + db.session.add(token) + db.session.commit() + r = self.client.get(path=url_for('selfservice.token_password', 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=token.token), + data={'password1': 'newpassword', 'password2': 'newpassword'}, follow_redirects=True) + dump('token_password_submit', r) + self.assertEqual(r.status_code, 200) + self.assertNotEqual(oldpw, get_ldap_password()) + # TODO: Verify that the new password is actually correct + self.assertEqual(len(PasswordToken.query.all()), 0) + + def test_token_password_emptydb(self): + user = User.from_ldap_dn('uid=testuser,ou=users,dc=example,dc=com') + oldpw = get_ldap_password() + r = self.client.get(path=url_for('selfservice.token_password', token='A'*128), follow_redirects=True) + dump('token_password_emptydb', r) + self.assertEqual(r.status_code, 200) + self.assertIn(b'Token expired, please try again', r.data) + r = self.client.post(path=url_for('selfservice.token_password', 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'Token expired, please try again', r.data) + self.assertEqual(oldpw, get_ldap_password()) + + def test_token_password_invalid(self): + user = User.from_ldap_dn('uid=testuser,ou=users,dc=example,dc=com') + oldpw = get_ldap_password() + token = PasswordToken(loginname=user.loginname) + db.session.add(token) + db.session.commit() + r = self.client.get(path=url_for('selfservice.token_password', token='A'*128), follow_redirects=True) + dump('token_password_invalid', r) + self.assertEqual(r.status_code, 200) + self.assertIn(b'Token expired, please try again', r.data) + r = self.client.post(path=url_for('selfservice.token_password', 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'Token expired, please try again', r.data) + self.assertEqual(oldpw, get_ldap_password()) + + def test_token_password_expired(self): + user = User.from_ldap_dn('uid=testuser,ou=users,dc=example,dc=com') + oldpw = get_ldap_password() + token = PasswordToken(loginname=user.loginname, + created=(datetime.datetime.now() - datetime.timedelta(days=10))) + db.session.add(token) + db.session.commit() + r = self.client.get(path=url_for('selfservice.token_password', token=token.token), follow_redirects=True) + dump('token_password_invalid_expired', r) + self.assertEqual(r.status_code, 200) + self.assertIn(b'Token expired, please try again', r.data) + r = self.client.post(path=url_for('selfservice.token_password', 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'Token expired, please try again', r.data) + self.assertEqual(oldpw, get_ldap_password()) + + def test_token_password_different_passwords(self): + user = User.from_ldap_dn('uid=testuser,ou=users,dc=example,dc=com') + oldpw = get_ldap_password() + token = PasswordToken(loginname=user.loginname) + db.session.add(token) + db.session.commit() + r = self.client.get(path=url_for('selfservice.token_password', token=token.token), follow_redirects=True) + self.assertEqual(r.status_code, 200) + r = self.client.post(path=url_for('selfservice.token_password', 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.assertEqual(oldpw, get_ldap_password()) + diff --git a/tests/test_session.py b/tests/test_session.py new file mode 100644 index 0000000000000000000000000000000000000000..fb66d2f21ea7fe7565dc9b151b6a5c07711a5de5 --- /dev/null +++ b/tests/test_session.py @@ -0,0 +1,136 @@ +import time +import unittest + +from flask import url_for + +# These imports are required, because otherwise we get circular imports?! +from uffd import ldap, user + +from uffd.session.views import get_current_user, login_required, is_valid_session +from uffd import create_app, db + +from utils import dump, UffdTestCase + +class TestSession(UffdTestCase): + def setUpApp(self): + self.app.config['SESSION_LIFETIME_SECONDS'] = 2 + + @self.app.route('/test_login_required') + @login_required() + def test_login_required(): + return 'SUCCESS', 200 + + @self.app.route('/test_group_required1') + @login_required(group='users') + def test_group_required1(): + return 'SUCCESS', 200 + + @self.app.route('/test_group_required2') + @login_required(group='notagroup') + def test_group_required2(): + return 'SUCCESS', 200 + + def setUp(self): + super().setUp() + self.assertFalse(is_valid_session()) + + def login(self): + self.client.post(path=url_for('session.login'), + data={'loginname': 'testuser', 'password': 'userpassword'}, follow_redirects=True) + self.assertTrue(is_valid_session()) + + def assertLogin(self): + self.assertTrue(is_valid_session()) + self.assertEqual(self.client.get(path=url_for('test_login_required'), + follow_redirects=True).data, b'SUCCESS') + self.assertEqual(get_current_user().loginname, 'testuser') + + def assertLogout(self): + self.assertFalse(is_valid_session()) + self.assertNotEqual(self.client.get(path=url_for('test_login_required'), + follow_redirects=True).data, b'SUCCESS') + self.assertEqual(get_current_user(), None) + + def test_login(self): + self.assertLogout() + r = self.client.get(path=url_for('session.login'), follow_redirects=True) + dump('login', r) + self.assertEqual(r.status_code, 200) + r = self.client.post(path=url_for('session.login'), + data={'loginname': 'testuser', 'password': 'userpassword'}, follow_redirects=True) + dump('login_post', r) + self.assertEqual(r.status_code, 200) + self.assertLogin() + + def test_redirect(self): + r = self.client.post(path=url_for('session.login', ref=url_for('test_login_required')), + data={'loginname': 'testuser', 'password': 'userpassword'}, follow_redirects=True) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.data, b'SUCCESS') + + def test_wrong_password(self): + r = self.client.post(path=url_for('session.login'), + data={'loginname': 'testuser', 'password': 'wrongpassword'}, follow_redirects=True) + dump('login_wrong_password', r) + self.assertEqual(r.status_code, 200) + self.assertLogout() + + @unittest.skip('See #27') + def test_empty_password(self): + r = self.client.post(path=url_for('session.login'), + data={'loginname': 'testuser', 'password': ''}, follow_redirects=True) + dump('login_empty_password', r) + self.assertEqual(r.status_code, 200) + self.assertLogout() + + def test_wrong_user(self): + r = self.client.post(path=url_for('session.login'), + data={'loginname': 'nouser', 'password': 'userpassword'}, follow_redirects=True) + dump('login_wrong_user', r) + self.assertEqual(r.status_code, 200) + self.assertLogout() + + def test_empty_user(self): + r = self.client.post(path=url_for('session.login'), + data={'loginname': '', 'password': 'userpassword'}, follow_redirects=True) + dump('login_empty_user', r) + self.assertEqual(r.status_code, 200) + self.assertLogout() + + def test_no_access(self): + r = self.client.post(path=url_for('session.login'), + data={'loginname': 'testservice', 'password': 'servicepassword'}, follow_redirects=True) + dump('login_no_access', r) + self.assertEqual(r.status_code, 200) + self.assertLogout() + + def test_group_required(self): + self.login() + self.assertEqual(self.client.get(path=url_for('test_group_required1'), + follow_redirects=True).data, b'SUCCESS') + self.assertNotEqual(self.client.get(path=url_for('test_group_required2'), + follow_redirects=True).data, b'SUCCESS') + + def test_logout(self): + self.login() + r = self.client.get(path=url_for('session.logout'), follow_redirects=True) + dump('logout', r) + self.assertEqual(r.status_code, 200) + self.assertLogout() + + @unittest.skip('See #29') + def test_timeout(self): + self.login() + time.sleep(3) + self.assertLogout() + + @unittest.skip('Not implemented, see #10') + def test_ratelimit(self): + for i in range(20): + self.client.post(path=url_for('session.login'), + data={'loginname': 'testuser', 'password': 'wrongpassword_%i'%i}, follow_redirects=True) + r = self.client.post(path=url_for('session.login'), + data={'loginname': 'testuser', 'password': 'userpassword'}, follow_redirects=True) + dump('login_ratelimit', r) + self.assertEqual(r.status_code, 200) + self.assertFalse(is_valid_session()) diff --git a/tests/test_user.py b/tests/test_user.py new file mode 100644 index 0000000000000000000000000000000000000000..3b095fd6ad9f1bb53db0110190cb62bfcb4da31b --- /dev/null +++ b/tests/test_user.py @@ -0,0 +1,316 @@ +import datetime +import time +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.session.views import get_current_user, is_valid_session +from uffd.mfa.models import MFAMethod, MFAType, RecoveryCodeMethod, TOTPMethod, WebauthnMethod, _hotp +from uffd import create_app, db + +from utils import dump, UffdTestCase + +def get_user(): + return User.from_ldap_dn('uid=testuser,ou=users,dc=example,dc=com') + +def get_user_password(): + conn = ldap.get_conn() + conn.search('uid=testuser,ou=users,dc=example,dc=com', '(objectClass=person)') + return conn.entries[0]['userPassword'] + +def get_admin(): + return User.from_ldap_dn('uid=testadmin,ou=users,dc=example,dc=com') + +class TestUserViews(UffdTestCase): + def setUp(self): + super().setUp() + self.client.post(path=url_for('session.login'), + data={'loginname': 'testadmin', 'password': 'adminpassword'}, follow_redirects=True) + + def test_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_new(self): + db.session.add(Role('base')) + role1 = Role('role1') + db.session.add(role1) + role2 = Role('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.from_ldap_dn('uid=newuser,ou=users,dc=example,dc=com')) + 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.from_ldap_dn('uid=newuser,ou=users,dc=example,dc=com') + roles = sorted([r.name for r in Role.get_for_user(user)]) + self.assertIsNotNone(user) + self.assertEqual(user.loginname, 'newuser') + self.assertEqual(user.displayname, 'New User') + self.assertEqual(user.mail, 'newuser@example.com') + self.assertTrue(user.uid) + self.assertEqual(roles, ['base', 'role1']) + # TODO: check password hash + + def test_new_invalid_loginname(self): + r = self.client.post(path=url_for('user.update'), + data={'loginname': '!newuser', 'mail': 'newuser@example.com', 'displayname': 'New User', + 'password': 'newpassword'}, follow_redirects=True) + dump('user_new_invalid_loginname', r) + self.assertEqual(r.status_code, 200) + self.assertIsNone(User.from_ldap_dn('uid=newuser,ou=users,dc=example,dc=com')) + + def test_new_empty_loginname(self): + r = self.client.post(path=url_for('user.update'), + data={'loginname': '', 'mail': 'newuser@example.com', 'displayname': 'New User', + 'password': 'newpassword'}, follow_redirects=True) + dump('user_new_empty_loginname', r) + self.assertEqual(r.status_code, 200) + self.assertIsNone(User.from_ldap_dn('uid=newuser,ou=users,dc=example,dc=com')) + + def test_new_empty_email(self): + r = self.client.post(path=url_for('user.update'), + data={'loginname': 'newuser', 'mail': '', 'displayname': 'New User', + 'password': 'newpassword'}, follow_redirects=True) + dump('user_new_empty_email', r) + self.assertEqual(r.status_code, 200) + self.assertIsNone(User.from_ldap_dn('uid=newuser,ou=users,dc=example,dc=com')) + + def test_new_invalid_display_name(self): + r = self.client.post(path=url_for('user.update'), + data={'loginname': 'newuser', 'mail': 'newuser@example.com', 'displayname': 'A'*200, + 'password': 'newpassword'}, follow_redirects=True) + dump('user_new_invalid_display_name', r) + self.assertEqual(r.status_code, 200) + self.assertIsNone(User.from_ldap_dn('uid=newuser,ou=users,dc=example,dc=com')) + + def test_update(self): + user = get_user() + db.session.add(Role('base')) + role1 = Role('role1') + db.session.add(role1) + role2 = Role('role2') + db.session.add(role2) + role2.add_member(user) + db.session.commit() + role1_id = role1.id + oldpw = get_user_password() + 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': 'testuser', 'mail': 'newuser@example.com', 'displayname': 'New User', + f'role-{role1_id}': '1', 'password': ''}, follow_redirects=True) + dump('user_update_submit', r) + self.assertEqual(r.status_code, 200) + _user = get_user() + roles = sorted([r.name for r in Role.get_for_user(_user)]) + self.assertEqual(_user.displayname, 'New User') + self.assertEqual(_user.mail, 'newuser@example.com') + self.assertEqual(_user.uid, user.uid) + self.assertEqual(_user.loginname, user.loginname) + self.assertEqual(get_user_password(), oldpw) + self.assertEqual(roles, ['base', 'role1']) + + def test_update_password(self): + user = get_user() + oldpw = get_user_password() + r = self.client.get(path=url_for('user.show', uid=user.uid), follow_redirects=True) + self.assertEqual(r.status_code, 200) + r = self.client.post(path=url_for('user.update', uid=user.uid), + data={'loginname': 'testuser', 'mail': 'newuser@example.com', 'displayname': 'New User', + 'password': 'newpassword'}, follow_redirects=True) + dump('user_update_password', r) + self.assertEqual(r.status_code, 200) + _user = get_user() + self.assertEqual(_user.displayname, 'New User') + self.assertEqual(_user.mail, 'newuser@example.com') + self.assertEqual(_user.uid, user.uid) + self.assertEqual(_user.loginname, user.loginname) + self.assertNotEqual(get_user_password(), oldpw) + + @unittest.skip('See #28') + def test_update_invalid_password(self): + user = get_user() + oldpw = get_user_password() + r = self.client.get(path=url_for('user.show', uid=user.uid), follow_redirects=True) + self.assertEqual(r.status_code, 200) + r = self.client.post(path=url_for('user.update', uid=user.uid), + data={'loginname': 'testuser', 'mail': 'newuser@example.com', 'displayname': 'New User', + 'password': 'A'}, follow_redirects=True) + dump('user_update_password', r) + self.assertEqual(r.status_code, 200) + _user = get_user() + self.assertEqual(get_user_password(), oldpw) + self.assertEqual(_user.displayname, user.displayname) + self.assertEqual(_user.mail, user.mail) + self.assertEqual(_user.loginname, user.loginname) + + def test_update_empty_email(self): + user = get_user() + oldpw = get_user_password() + r = self.client.get(path=url_for('user.show', uid=user.uid), follow_redirects=True) + self.assertEqual(r.status_code, 200) + r = self.client.post(path=url_for('user.update', uid=user.uid), + data={'loginname': 'testuser', 'mail': '', 'displayname': 'New User', + 'password': 'newpassword'}, follow_redirects=True) + dump('user_update_empty_mail', r) + self.assertEqual(r.status_code, 200) + _user = get_user() + self.assertEqual(_user.displayname, user.displayname) + self.assertEqual(_user.mail, user.mail) + self.assertEqual(_user.loginname, user.loginname) + self.assertEqual(get_user_password(), oldpw) + + def test_update_invalid_display_name(self): + user = get_user() + oldpw = get_user_password() + r = self.client.get(path=url_for('user.show', uid=user.uid), follow_redirects=True) + self.assertEqual(r.status_code, 200) + r = self.client.post(path=url_for('user.update', uid=user.uid), + data={'loginname': 'testuser', 'mail': 'newuser@example.com', 'displayname': 'A'*200, + 'password': 'newpassword'}, follow_redirects=True) + dump('user_update_invalid_display_name', r) + self.assertEqual(r.status_code, 200) + _user = get_user() + self.assertEqual(_user.displayname, user.displayname) + self.assertEqual(_user.mail, user.mail) + self.assertEqual(_user.loginname, user.loginname) + self.assertEqual(get_user_password(), oldpw) + + def test_show(self): + r = self.client.get(path=url_for('user.show', uid=get_user().uid), follow_redirects=True) + dump('user_show', r) + self.assertEqual(r.status_code, 200) + + def test_delete(self): + user = get_user() + r = self.client.get(path=url_for('user.delete', uid=user.uid), follow_redirects=True) + dump('user_delete', r) + self.assertEqual(r.status_code, 200) + self.assertIsNone(get_user()) + + def test_csvimport(self): + db.session.add(Role('base')) + role1 = Role('role1') + db.session.add(role1) + role2 = Role('role2') + db.session.add(role2) + db.session.commit() + data = f'''\ +newuser1,newuser1@example.com, +newuser2,newuser2@example.com,{role1.id} +newuser3,newuser3@example.com,{role1.id};{role2.id} +newuser4,newuser4@example.com,9999 +newuser5,newuser5@example.com,notanumber +newuser6,newuser6@example.com,{role1.id};{role2.id}; +newuser7,invalidmail, +newuser8,, +,newuser9@example.com, +,, + +,,, +newuser10,newuser10@example.com, +newuser11,newuser11@example.com, {role1.id};{role2.id} +newuser12,newuser12@example.com,{role1.id};{role1.id} +<invalid tag-like thingy>''' + r = self.client.post(path=url_for('user.csvimport'), data={'csv': data}, follow_redirects=True) + dump('user_csvimport', r) + self.assertEqual(r.status_code, 200) + user = User.from_ldap_dn('uid=newuser1,ou=users,dc=example,dc=com') + roles = sorted([r.name for r in Role.get_for_user(user)]) + self.assertIsNotNone(user) + self.assertEqual(user.loginname, 'newuser1') + self.assertEqual(user.displayname, 'newuser1') + self.assertEqual(user.mail, 'newuser1@example.com') + self.assertEqual(roles, ['base']) + user = User.from_ldap_dn('uid=newuser2,ou=users,dc=example,dc=com') + roles = sorted([r.name for r in Role.get_for_user(user)]) + self.assertIsNotNone(user) + self.assertEqual(user.loginname, 'newuser2') + self.assertEqual(user.displayname, 'newuser2') + self.assertEqual(user.mail, 'newuser2@example.com') + self.assertEqual(roles, ['base', 'role1']) + user = User.from_ldap_dn('uid=newuser3,ou=users,dc=example,dc=com') + roles = sorted([r.name for r in Role.get_for_user(user)]) + self.assertIsNotNone(user) + self.assertEqual(user.loginname, 'newuser3') + self.assertEqual(user.displayname, 'newuser3') + self.assertEqual(user.mail, 'newuser3@example.com') + self.assertEqual(roles, ['base', 'role1', 'role2']) + user = User.from_ldap_dn('uid=newuser4,ou=users,dc=example,dc=com') + roles = sorted([r.name for r in Role.get_for_user(user)]) + self.assertIsNotNone(user) + self.assertEqual(user.loginname, 'newuser4') + self.assertEqual(user.displayname, 'newuser4') + self.assertEqual(user.mail, 'newuser4@example.com') + self.assertEqual(roles, ['base']) + user = User.from_ldap_dn('uid=newuser5,ou=users,dc=example,dc=com') + roles = sorted([r.name for r in Role.get_for_user(user)]) + self.assertIsNotNone(user) + self.assertEqual(user.loginname, 'newuser5') + self.assertEqual(user.displayname, 'newuser5') + self.assertEqual(user.mail, 'newuser5@example.com') + self.assertEqual(roles, ['base']) + user = User.from_ldap_dn('uid=newuser6,ou=users,dc=example,dc=com') + roles = sorted([r.name for r in Role.get_for_user(user)]) + self.assertIsNotNone(user) + self.assertEqual(user.loginname, 'newuser6') + self.assertEqual(user.displayname, 'newuser6') + self.assertEqual(user.mail, 'newuser6@example.com') + self.assertEqual(roles, ['base', 'role1', 'role2']) + self.assertIsNone(User.from_ldap_dn('uid=newuser7,ou=users,dc=example,dc=com')) + self.assertIsNone(User.from_ldap_dn('uid=newuser8,ou=users,dc=example,dc=com')) + self.assertIsNone(User.from_ldap_dn('uid=newuser9,ou=users,dc=example,dc=com')) + user = User.from_ldap_dn('uid=newuser10,ou=users,dc=example,dc=com') + roles = sorted([r.name for r in Role.get_for_user(user)]) + self.assertIsNotNone(user) + self.assertEqual(user.loginname, 'newuser10') + self.assertEqual(user.displayname, 'newuser10') + self.assertEqual(user.mail, 'newuser10@example.com') + self.assertEqual(roles, ['base']) + user = User.from_ldap_dn('uid=newuser11,ou=users,dc=example,dc=com') + roles = sorted([r.name for r in Role.get_for_user(user)]) + self.assertIsNotNone(user) + self.assertEqual(user.loginname, 'newuser11') + self.assertEqual(user.displayname, 'newuser11') + self.assertEqual(user.mail, 'newuser11@example.com') + # Currently the csv import is not very robust, imho newuser11 should have role1 and role2! + #self.assertEqual(roles, ['base', 'role1', 'role2']) + self.assertEqual(roles, ['base', 'role2']) + user = User.from_ldap_dn('uid=newuser12,ou=users,dc=example,dc=com') + roles = sorted([r.name for r in Role.get_for_user(user)]) + self.assertIsNotNone(user) + self.assertEqual(user.loginname, 'newuser12') + self.assertEqual(user.displayname, 'newuser12') + self.assertEqual(user.mail, 'newuser12@example.com') + self.assertEqual(roles, ['base', 'role1']) + +class TestGroupViews(UffdTestCase): + def setUp(self): + super().setUp() + self.client.post(path=url_for('session.login'), + data={'loginname': 'testadmin', 'password': 'adminpassword'}, follow_redirects=True) + + def test_index(self): + r = self.client.get(path=url_for('group.index'), follow_redirects=True) + dump('group_index', r) + self.assertEqual(r.status_code, 200) + + def test_show(self): + r = self.client.get(path=url_for('group.show', gid=20001), follow_redirects=True) + dump('group_show', r) + self.assertEqual(r.status_code, 200) + diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..8cdd4772c5287fecd3876918a7ac31acdd3ce674 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,43 @@ +import os +import tempfile +import shutil +import unittest + +from uffd import create_app, db + +def dump(basename, resp): + basename = basename.replace('.', '_').replace('/', '_') + suffix = '.html' + root = os.environ.get('DUMP_PAGES') + if not root: + return + os.makedirs(root, exist_ok=True) + path = os.path.join(root, basename+suffix) + with open(path, 'xb') as f: + f.write(resp.data) + +class UffdTestCase(unittest.TestCase): + def setUp(self): + self.dir = tempfile.mkdtemp() + # It would be far better to create a minimal app here, but since the + # session module depends on almost everything else, that is not really feasable + self.app = create_app({ + 'TESTING': True, + 'DEBUG': True, + 'SQLALCHEMY_DATABASE_URI': 'sqlite:///%s/db.sqlite'%self.dir, + 'SECRET_KEY': 'DEBUGKEY', + 'LDAP_SERVICE_MOCK': True, + }) + self.setUpApp() + self.client = self.app.test_client() + self.client.__enter__() + # Just do some request so that we can use url_for + self.client.get(path='/') + db.create_all() + + def setUpApp(self): + pass + + def tearDown(self): + self.client.__exit__(None, None, None) + shutil.rmtree(self.dir) diff --git a/uffd/mail/templates/mail.html b/uffd/mail/templates/mail.html index 36021697550b986fe5902ae9e13a0084b74f3cff..44f74f47264aec0bba60bea128e507d2ba9a892b 100644 --- a/uffd/mail/templates/mail.html +++ b/uffd/mail/templates/mail.html @@ -11,14 +11,14 @@ </div> <div class="form-group col"> <label for="mail-receivers">Receiving addresses</label> - <textarea rows="10" class="form-control" name="mail-receivers">{{ mail.receivers|join('\n') }}</textarea> + <textarea rows="10" class="form-control" id="mail-receivers" name="mail-receivers">{{ mail.receivers|join('\n') }}</textarea> <small class="form-text text-muted"> One address per line </small> </div> <div class="form-group col"> <label for="mail-destinations">Destinations</label> - <textarea rows="10" class="form-control" name="mail-destinations">{{ mail.destinations|join('\n') }}</textarea> + <textarea rows="10" class="form-control" id="mail-destinations" name="mail-destinations">{{ mail.destinations|join('\n') }}</textarea> <small class="form-text text-muted"> One address per line </small> diff --git a/uffd/mail/templates/mail_list.html b/uffd/mail/templates/mail_list.html index 32fa3b6448a407221ae4c9fa245a3ecf959e795c..844689516dc0d78d21a2126b4d886319db1ad6ff 100644 --- a/uffd/mail/templates/mail_list.html +++ b/uffd/mail/templates/mail_list.html @@ -11,7 +11,7 @@ <th scope="col">destinations</th> <th scope="col"> <p class="text-right"> - <a type="button" class="btn btn-primary" href="{{ url_for("mail.show") }}"> + <a class="btn btn-primary" href="{{ url_for("mail.show") }}"> <i class="fa fa-plus" aria-hidden="true"></i> New </a> </p> @@ -52,5 +52,5 @@ </tbody> </table> </div> -</dev> +</div> {% endblock %} diff --git a/uffd/mfa/templates/auth.html b/uffd/mfa/templates/auth.html index 831bc87a74ed93418ee6ec6139765cf0ac656c7b..13c1f0c60add51f9ed25eddc3a8c794c57823af9 100644 --- a/uffd/mfa/templates/auth.html +++ b/uffd/mfa/templates/auth.html @@ -6,7 +6,7 @@ <div class="row mt-2 justify-content-center"> <div class="col-lg-6 col-md-10" style="background: #f7f7f7; box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); padding: 30px;"> <div class="text-center"> - <img src="{{ url_for("static", filename="chaosknoten.png") }}" class="col-lg-8 col-md-12" > + <img alt="CCC logo" src="{{ url_for("static", filename="chaosknoten.png") }}" class="col-lg-8 col-md-12" > </div> <div class="col-12 mb-3"> <h2 class="text-center">Two-Factor Authentication</h2> diff --git a/uffd/role/templates/role_list.html b/uffd/role/templates/role_list.html index 007ddf387db50f98572201800448e147d790ebd9..1e2f31fa6e17394a3397c533b747f37cd7a0277e 100644 --- a/uffd/role/templates/role_list.html +++ b/uffd/role/templates/role_list.html @@ -11,7 +11,7 @@ <th scope="col">description</th> <th scope="col"> <p class="text-right"> - <a type="button" class="btn btn-primary" href="{{ url_for("role.show") }}"> + <a class="btn btn-primary" href="{{ url_for("role.show") }}"> <i class="fa fa-plus" aria-hidden="true"></i> New </a> </p> @@ -42,5 +42,5 @@ </tbody> </table> </div> -</dev> +</div> {% endblock %} diff --git a/uffd/selfservice/templates/forgot_password.html b/uffd/selfservice/templates/forgot_password.html index 641f7dcb982189ffefcf07add68283239dee2a25..9cc647e0bb36f7fd694dd946c0acd549dd3f2b75 100644 --- a/uffd/selfservice/templates/forgot_password.html +++ b/uffd/selfservice/templates/forgot_password.html @@ -5,7 +5,7 @@ <div class="row mt-2 justify-content-center"> <div class="col-lg-6 col-md-10" style="background: #f7f7f7; box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); padding: 30px;"> <div class="text-center"> - <img src="{{ url_for("static", filename="chaosknoten.png") }}" class="col-lg-8 col-md-12" > + <img alt="CCC logo" src="{{ url_for("static", filename="chaosknoten.png") }}" class="col-lg-8 col-md-12" > </div> <div class="col-12"> <h2 class="text-center">Forgot password</h2> diff --git a/uffd/selfservice/templates/set_password.html b/uffd/selfservice/templates/set_password.html index 67f3a5a2ca4dc67c3400fbd8977f1a520537ac96..50569bc372289554f24ed622bc715f9888612d7f 100644 --- a/uffd/selfservice/templates/set_password.html +++ b/uffd/selfservice/templates/set_password.html @@ -5,7 +5,7 @@ <div class="row mt-2 justify-content-center"> <div class="col-lg-6 col-md-10" style="background: #f7f7f7; box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); padding: 30px;"> <div class="text-center"> - <img src="{{ url_for("static", filename="chaosknoten.png") }}" class="col-lg-8 col-md-12" > + <img alt="CCC logo" src="{{ url_for("static", filename="chaosknoten.png") }}" class="col-lg-8 col-md-12" > </div> <div class="col-12"> <h2 class="text-center">Reset password</h2> diff --git a/uffd/selfservice/views.py b/uffd/selfservice/views.py index bcefe55fadb6a10ebecf8e38ed6941985dbacc61..4770c950f0d8aaca587be67ea127e6d56d854230 100644 --- a/uffd/selfservice/views.py +++ b/uffd/selfservice/views.py @@ -145,17 +145,27 @@ def send_passwordreset(loginname): send_mail(user.mail, msg) def send_mail(to_address, msg): + msg['From'] = current_app.config['MAIL_FROM_ADDRESS'] + msg['To'] = to_address + msg['Date'] = email.utils.formatdate(localtime=1) + msg['Message-ID'] = email.utils.make_msgid() try: + if current_app.debug: + current_app.last_mail = None + current_app.logger.debug('Trying to send email to %s:\n'%(to_address)+str(msg)) + if current_app.debug and current_app.config.get('MAIL_SKIP_SEND', False): + if current_app.config['MAIL_SKIP_SEND'] == 'fail': + raise smtplib.SMTPException() + current_app.last_mail = msg + return True server = smtplib.SMTP(host=current_app.config['MAIL_SERVER'], port=current_app.config['MAIL_PORT']) if current_app.config['MAIL_USE_STARTTLS']: server.starttls() server.login(current_app.config['MAIL_USERNAME'], current_app.config['MAIL_PASSWORD']) - msg['From'] = current_app.config['MAIL_FROM_ADDRESS'] - msg['To'] = to_address - msg['Date'] = email.utils.formatdate(localtime=1) - msg['Message-ID'] = email.utils.make_msgid() server.send_message(msg) server.quit() + if current_app.debug: + current_app.last_mail = msg return True except smtplib.SMTPException: flash('Mail to "{}" could not be sent!'.format(to_address)) diff --git a/uffd/session/templates/login.html b/uffd/session/templates/login.html index 0332394891e194433e77b0b57500461164511c82..63189ed099a7157359dee4ddd50770451560d37e 100644 --- a/uffd/session/templates/login.html +++ b/uffd/session/templates/login.html @@ -5,7 +5,7 @@ <div class="row mt-2 justify-content-center"> <div class="col-lg-6 col-md-10" style="background: #f7f7f7; box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); padding: 30px;"> <div class="text-center"> - <img src="{{ url_for("static", filename="chaosknoten.png") }}" class="col-lg-8 col-md-12" > + <img alt="CCC logo" src="{{ url_for("static", filename="chaosknoten.png") }}" class="col-lg-8 col-md-12" > </div> <div class="col-12"> <h2 class="text-center">Login</h2> diff --git a/uffd/template_helper.py b/uffd/template_helper.py index 998bafaa4ecf3a7e745e226c7ddb73c7de25226f..cc02d55621fb506141e3ef5fbd9ba01149af018c 100644 --- a/uffd/template_helper.py +++ b/uffd/template_helper.py @@ -22,7 +22,7 @@ def register_template_helper(app): svg.set(key, value) buf = io.BytesIO() img.save(buf) - return Markup(buf.getvalue().decode()) + return Markup(buf.getvalue().decode().replace('<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n', '')) @app.template_filter() def datauri(data, mimetype='text/plain'): #pylint: disable=unused-variable diff --git a/uffd/user/templates/group.html b/uffd/user/templates/group.html index 6ca116ea5005eaa8c3fc0c328a25e83a49343948..2bfcebd418c1d57c6feba66d6fbe2e5565bdc7fc 100644 --- a/uffd/user/templates/group.html +++ b/uffd/user/templates/group.html @@ -8,7 +8,7 @@ <input type="number" class="form-control" id="group-gid" name="gid" value="{{ group.gid }}" readonly> </div> <div class="form-group col"> - <label for="group-name">name</label> + <label for="group-loginname">name</label> <input type="text" class="form-control" id="group-loginname" name="loginname" value="{{ group.name }}" readonly> </div> <div class="col"> diff --git a/uffd/user/templates/group_list.html b/uffd/user/templates/group_list.html index eca01e423bff881454af86019d201d339a8e854f..3b3a41dfbbedf42f7cce67609409e71a8d5c0f77 100644 --- a/uffd/user/templates/group_list.html +++ b/uffd/user/templates/group_list.html @@ -30,5 +30,5 @@ </tbody> </table> </div> -</dev> +</div> {% endblock %} diff --git a/uffd/user/templates/user.html b/uffd/user/templates/user.html index aa10e9ea7723b37c48d5c495e31bead1350de0b0..1411c9f12269143f62b7c80bfe05704ca5337651 100644 --- a/uffd/user/templates/user.html +++ b/uffd/user/templates/user.html @@ -31,7 +31,7 @@ {% if user.uid %} <input type="number" class="form-control" id="user-uid" name="uid" value="{{ user.uid }}" readonly> {% else %} - <input type="string" class="form-control" id="user-uid" name="uid" placeholder="will be choosen" readonly> + <input type="text" class="form-control" id="user-uid" name="uid" placeholder="will be choosen" readonly> {% endif %} </div> <div class="form-group col"> diff --git a/uffd/user/templates/user_list.html b/uffd/user/templates/user_list.html index 21d437c38749670ebb7063f9996acb1637d32d12..34102af796966bbbcd35c90c2ed2557b4bda423d 100644 --- a/uffd/user/templates/user_list.html +++ b/uffd/user/templates/user_list.html @@ -11,7 +11,7 @@ <th scope="col">display name</th> <th scope="col"> <p class="text-right"> - <a type="button" class="btn btn-primary" href="{{ url_for("user.show") }}"> + <a class="btn btn-primary" href="{{ url_for("user.show") }}"> <i class="fa fa-plus" aria-hidden="true"></i> New </a> <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#csvimport"> @@ -61,8 +61,8 @@ </div> <div class="modal-body"> <p> - The format should be "loginname,mailaddres,groupid1;groupid2;". - Neither setting the display name, nor setting roles or passwords is supported (yet). + The format should be "loginname,mailaddres,roleid1;roleid2". + Neither setting the display name nor setting passwords is supported (yet). Example: </p> <pre>