diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..89e6538abc4ecc13cd139f1d025f991ebe18a894 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fabf8d89dfb2598badfdb87ea5ed85d123345be6..1596caaa6ccd93c04d3f5af879d081e2f28944ec 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,23 +10,72 @@ before_script: - python3 -m pylint --version - python3 -m coverage --version -linter: +linter:buster: + image: registry.git.cccv.de/uffd/docker-images/buster stage: test script: - - python3 -m pylint --rcfile .pylintrc --output-format=text app.py | tee pylint.txt + - pip3 install pylint-gitlab + - python3 -m pylint --exit-zero --rcfile .pylintrc --output-format=pylint_gitlab.GitlabCodeClimateReporter app.py > codeclimate.json + - python3 -m pylint --exit-zero --rcfile .pylintrc --output-format=pylint_gitlab.GitlabPagesHtmlReporter app.py > pylint.html + - python3 -m pylint --rcfile .pylintrc --output-format=text app.py 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 +linter:bullseye: + image: registry.git.cccv.de/uffd/docker-images/bullseye + stage: test + script: + - pip3 install pylint-gitlab + - python3 -m pylint --exit-zero --rcfile .pylintrc --output-format=pylint_gitlab.GitlabCodeClimateReporter app.py > codeclimate.json + - python3 -m pylint --exit-zero --rcfile .pylintrc --output-format=pylint_gitlab.GitlabPagesHtmlReporter app.py > pylint.html + - python3 -m pylint --rcfile .pylintrc --output-format=text app.py + artifacts: + when: always + paths: + - pylint.html + reports: + codequality: codeclimate.json + +unittests:buster: + image: registry.git.cccv.de/uffd/docker-images/buster + stage: test + script: + - service slapd start + - UNITTEST_OPENLDAP=1 python3-coverage run --include 'app.py' -m pytest --junitxml=report.xml || true + - 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+\%)$/' + +unittests:bullseye: + image: registry.git.cccv.de/uffd/docker-images/bullseye + stage: test + script: + - service slapd start + - UNITTEST_OPENLDAP=1 python3-coverage run --include 'app.py' -m pytest --junitxml=report.xml || true + #- 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+\%)$/' diff --git a/.pylintrc b/.pylintrc index 5e758da8d9174f0ddd776d96ecaf9f4530b75d3e..15f00d04eee663eef83ad36a0ca442db52530196 100644 --- a/.pylintrc +++ b/.pylintrc @@ -63,90 +63,6 @@ confidence= disable=missing-module-docstring, missing-class-docstring, missing-function-docstring, - print-statement, - parameter-unpacking, - unpacking-in-except, - old-raise-syntax, - backtick, - long-suffix, - old-ne-operator, - old-octal-literal, - import-star-module-level, - non-ascii-bytes-literal, - raw-checker-failed, - bad-inline-option, - locally-disabled, - file-ignored, - suppressed-message, - useless-suppression, - deprecated-pragma, - use-symbolic-message-instead, - unused-wildcard-import, - apply-builtin, - basestring-builtin, - buffer-builtin, - cmp-builtin, - coerce-builtin, - execfile-builtin, - file-builtin, - long-builtin, - raw_input-builtin, - reduce-builtin, - standarderror-builtin, - unicode-builtin, - xrange-builtin, - coerce-method, - delslice-method, - getslice-method, - setslice-method, - no-absolute-import, - old-division, - dict-iter-method, - dict-view-method, - next-method-called, - metaclass-assignment, - indexing-exception, - raising-string, - reload-builtin, - oct-method, - hex-method, - nonzero-method, - cmp-method, - input-builtin, - round-builtin, - intern-builtin, - unichr-builtin, - map-builtin-not-iterating, - zip-builtin-not-iterating, - range-builtin-not-iterating, - filter-builtin-not-iterating, - using-cmp-argument, - eq-without-hash, - div-method, - idiv-method, - rdiv-method, - exception-message-attribute, - invalid-str-codec, - sys-max-int, - bad-python3-import, - deprecated-string-function, - deprecated-str-translate-call, - deprecated-itertools-function, - deprecated-types-field, - next-method-defined, - dict-items-not-iterating, - dict-keys-not-iterating, - dict-values-not-iterating, - deprecated-operator-function, - deprecated-urllib-function, - xreadlines-attribute, - deprecated-sys-function, - exception-escape, - comprehension-escape, - too-few-public-methods, - method-hidden, - bad-continuation, - unused-variable, # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/default_config.py b/default_config.py index 4d3d06163730cda9b6e2838fcd5e344d9fee7e7e..b36dd3a2e4b99f77c890539faffcefe82d393cc2 100644 --- a/default_config.py +++ b/default_config.py @@ -1,5 +1,5 @@ -# OAuthProxy will usually served from the same domain as the services that -# use it for OAuth integration, so make sure that the session cookie does +# OAuthProxy is usually served from the same domain as the service that +# uses it for OAuth2 integration. Make sure that the session cookie does # not conflict with any other cookies! SESSION_COOKIE_NAME = 'oauth-session' diff --git a/test_app.py b/test_app.py new file mode 100644 index 0000000000000000000000000000000000000000..f72e356f1885ee1fc884fb12ed823b048b41934b --- /dev/null +++ b/test_app.py @@ -0,0 +1,164 @@ +import unittest +try: + import mock +except ImportError: + from unittest import mock +import json +import urllib.parse + +from flask import session +from requests import Session, Response + +from app import create_app + +headers = { + 'X-CLIENT-ID': 'test_client_id', + 'X-CLIENT-SECRET': 'test_client_secret', + 'X-REDIRECT-URI': 'https://127.0.0.123:7654/callback', +} + +class MockRequest: + def __init__(self): + self.headers = [] + self.body = '' + +class MockResponse: + def __init__(self, status_code, json_data=None): + self.request = MockRequest() + self.ok = status_code == 200 + self.status_code = status_code + self.json = lambda: json_data + self.headers = [] + self.text = json.dumps(json_data) + +def mock_request(self, method, url, **kwargs): + if method == 'POST' and url == 'https://127.0.0.123:4567/token': + return MockResponse(200, {'access_token': '2YotnFZFEjr1zCsicMWpAA', + 'token_type': 'Bearer', + 'expires_in': 3600, + 'refresh_token': 'tGzv3JOkF0XG5Qx2TlKWIA'}) + if method == 'GET' and url == 'https://127.0.0.123:4567/userinfo': + if kwargs['headers']['Authorization'] != 'Bearer 2YotnFZFEjr1zCsicMWpAA': + raise Exception() + return MockResponse(200, {'id': 1234, + 'name': 'Test User', + 'nickname': 'testuser', + 'email': 'test@example.com', + 'ldap_dn': 'uid=testuser,ou=users,dc=example,dc=com', + 'groups': ['uffd_access', 'users']}) + print(repr(method), repr(url), repr(kwargs)) + raise Exception() + +@mock.patch.object(Session, 'request', new=mock_request) +class TestCases(unittest.TestCase): + def setUp(self): + config = { + 'TESTING': True, + 'DEBUG': True, + 'SECRET_KEY': 'DEBUGKEY', + 'OAUTH2_AUTH_URL': 'https://127.0.0.123:4567/authorize', + 'OAUTH2_TOKEN_URL': 'https://127.0.0.123:4567/token', + 'OAUTH2_USERINFO_URL': 'https://127.0.0.123:4567/userinfo', + } + self.app = create_app(config) + 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 test_status(self): + r = self.client.get(path='/status', headers=headers) + self.assertEqual(r.status_code, 200) + self.assertIn('test_client_id', r.data.decode()) + self.assertNotIn('test_client_secret', r.data.decode()) + + def test_auth_no_session(self): + r = self.client.get(path='/auth', headers=headers) + self.assertEqual(r.status_code, 401) + + def test_login(self): + r = self.client.get(path='/login', query_string={'url': 'https://127.0.0.123:7654/app'}, headers=headers, follow_redirects=False) + self.assertEqual(r.status_code, 302) + url = urllib.parse.urlparse(r.location) + qs = urllib.parse.parse_qs(url.query) + self.assertEqual(url.scheme, 'https') + self.assertEqual(url.netloc, '127.0.0.123:4567') + self.assertEqual(url.path, '/authorize') + self.assertEqual(qs['response_type'], ['code']) + self.assertEqual(qs['client_id'], ['test_client_id']) + self.assertEqual(qs['redirect_uri'], ['https://127.0.0.123:7654/callback']) + self.assertGreater(len(qs['state'][0]), 8) + self.assertEqual(session['state'], qs['state'][0]) + self.assertEqual(session['url'], 'https://127.0.0.123:7654/app') + + def test_callback(self): + code = 'testcode' + state = 'teststate' + with self.client.session_transaction() as session: + session['state'] = state + session['url'] = 'https://127.0.0.123:7654/app' + r = self.client.get(path='/callback', headers=headers, query_string={'code': code, 'state': state}, follow_redirects=False) + self.assertEqual(r.status_code, 302) + self.assertEqual(r.location, 'https://127.0.0.123:7654/app') + with self.client.session_transaction() as session: + self.assertEqual(session['user_id'], 1234) + self.assertEqual(session['user_name'], 'Test User') + self.assertEqual(session['user_nickname'], 'testuser') + self.assertEqual(session['user_email'], 'test@example.com') + self.assertEqual(session['user_ldap_dn'], 'uid=testuser,ou=users,dc=example,dc=com') + self.assertEqual(set(session['user_groups']), set(['uffd_access', 'users'])) + self.assertNotIn('state', session) + self.assertNotIn('url', session) + + def test_auth_session(self): + with self.client.session_transaction() as session: + session['user_id'] = 1234 + session['user_name'] = 'Test User' + session['user_nickname'] = 'testuser' + session['user_email'] = 'test@example.com' + session['user_ldap_dn'] = 'uid=testuser,ou=users,dc=example,dc=com' + session['user_groups'] = ['uffd_access', 'users'] + r = self.client.get(path='/auth', headers=headers) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.headers['OAUTH-USER-ID'], '1234') + self.assertEqual(r.headers['OAUTH-USER-NAME'], 'Test User') + self.assertEqual(r.headers['OAUTH-USER-NICKNAME'], 'testuser') + self.assertEqual(r.headers['OAUTH-USER-EMAIL'], 'test@example.com') + self.assertEqual(r.headers['OAUTH-USER-LDAP-DN'], 'uid=testuser,ou=users,dc=example,dc=com') + self.assertIn(r.headers['OAUTH-USER-GROUPS'], ['uffd_access,users', 'users,uffd_access']) + + def test_logout(self): + with self.client.session_transaction() as session: + session['user_id'] = 1234 + session['user_name'] = 'Test User' + session['user_nickname'] = 'testuser' + session['user_email'] = 'test@example.com' + session['user_ldap_dn'] = 'uid=testuser,ou=users,dc=example,dc=com' + session['user_groups'] = ['uffd_access', 'users'] + r = self.client.get(path='/logout', headers=headers) + self.assertEqual(r.status_code, 200) + with self.client.session_transaction() as session: + self.assertEqual(list(session.keys()), []) + + def test_logout_no_session(self): + r = self.client.get(path='/logout', headers=headers) + self.assertEqual(r.status_code, 200) + with self.client.session_transaction() as session: + self.assertEqual(list(session.keys()), []) + + def test_logout_redirect(self): + with self.client.session_transaction() as session: + session['user_id'] = 1234 + session['user_name'] = 'Test User' + session['user_nickname'] = 'testuser' + session['user_email'] = 'test@example.com' + session['user_ldap_dn'] = 'uid=testuser,ou=users,dc=example,dc=com' + session['user_groups'] = ['uffd_access', 'users'] + r = self.client.get(path='/logout', headers=headers, query_string={'redirect_url': 'https://127.0.0.123:7654/app/logout'}) + self.assertEqual(r.status_code, 302) + self.assertEqual(r.location, 'https://127.0.0.123:7654/app/logout') + with self.client.session_transaction() as session: + self.assertEqual(list(session.keys()), [])