diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a653bb475..72712bbd59 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,21 +9,21 @@ on: jobs: lint: - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 strategy: matrix: python-version: [3.8] steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y python-dev libldap2-dev libsasl2-dev libssl-dev + sudo apt-get install -y libldap2-dev libsasl2-dev libssl-dev pip install --upgrade pip pip install -r requirements.txt pip install -r requirements-dev.txt @@ -35,10 +35,10 @@ jobs: run: mypy flask_appbuilder test-postgres: - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 strategy: matrix: - python-version: [3.7, 3.8, 3.9.7] + python-version: ["3.7", "3.8", "3.9"] env: SQLALCHEMY_DATABASE_URI: postgresql+psycopg2://pguser:pguserpassword@127.0.0.1:15432/app @@ -52,15 +52,27 @@ jobs: ports: - 15432:5432 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + - name: Run openldap + run: | + docker run -d \ + -v '${{ github.workspace }}/docker/openldap/ldifs:/ldifs' \ + -v '${{ github.workspace }}/docker/openldap/schemas/memberof.ldif:/opt/bitnami/openldap/etc/schema/memberof.ldif' \ + -e LDAP_URI=ldap://openldap:1389 \ + -e LDAP_BASE=dc=example,dc=org \ + -e LDAP_ADMIN_USERNAME=admin \ + -e LDAP_ADMIN_PASSWORD=admin_password \ + -e LDAP_EXTRA_SCHEMAS=cosine,inetorgperson,nis,memberof \ + -p 1389:1389 \ + bitnami/openldap:2.6.4 - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y python-dev libldap2-dev libsasl2-dev libssl-dev + sudo apt-get install -y libldap2-dev libsasl2-dev libssl-dev pip install --upgrade pip pip install -r requirements.txt pip install -r requirements-dev.txt @@ -73,10 +85,10 @@ jobs: bash <(curl -s https://codecov.io/bash) -cF python test-mysql: - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 strategy: matrix: - python-version: [3.7] + python-version: [3.8] env: SQLALCHEMY_DATABASE_URI: | mysql+mysqldb://mysqluser:mysqluserpassword@127.0.0.1:13306/app?charset=utf8mb4&binary_prefix=true @@ -91,15 +103,27 @@ jobs: ports: - 13306:3306 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + - name: Run openldap + run: | + docker run -d \ + -v '${{ github.workspace }}/docker/openldap/ldifs:/ldifs' \ + -v '${{ github.workspace }}/docker/openldap/schemas/memberof.ldif:/opt/bitnami/openldap/etc/schema/memberof.ldif' \ + -e LDAP_URI=ldap://openldap:1389 \ + -e LDAP_BASE=dc=example,dc=org \ + -e LDAP_ADMIN_USERNAME=admin \ + -e LDAP_ADMIN_PASSWORD=admin_password \ + -e LDAP_EXTRA_SCHEMAS=cosine,inetorgperson,nis,memberof \ + -p 1389:1389 \ + bitnami/openldap:2.6.4 - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y python-dev libldap2-dev libsasl2-dev libssl-dev + sudo apt-get install -y libldap2-dev libsasl2-dev libssl-dev pip install --upgrade pip pip install -r requirements.txt pip install -r requirements-dev.txt @@ -112,10 +136,10 @@ jobs: bash <(curl -s https://codecov.io/bash) -cF python test-mssql: - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 strategy: matrix: - python-version: [3.7] + python-version: [3.8] env: SQLALCHEMY_DATABASE_URI: | mssql+pyodbc://sa:Password_123@localhost:11433/master?driver=FreeTDS @@ -128,15 +152,27 @@ jobs: ports: - 11433:1433 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + - name: Run openldap + run: | + docker run -d \ + -v '${{ github.workspace }}/docker/openldap/ldifs:/ldifs' \ + -v '${{ github.workspace }}/docker/openldap/schemas/memberof.ldif:/opt/bitnami/openldap/etc/schema/memberof.ldif' \ + -e LDAP_URI=ldap://openldap:1389 \ + -e LDAP_BASE=dc=example,dc=org \ + -e LDAP_ADMIN_USERNAME=admin \ + -e LDAP_ADMIN_PASSWORD=admin_password \ + -e LDAP_EXTRA_SCHEMAS=cosine,inetorgperson,nis,memberof \ + -p 1389:1389 \ + bitnami/openldap:2.6.4 - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y python-dev libldap2-dev libsasl2-dev libssl-dev freetds-bin unixodbc-dev tdsodbc + sudo apt-get install -y libldap2-dev libsasl2-dev libssl-dev freetds-bin unixodbc-dev tdsodbc pip install --upgrade pip pip install -r requirements.txt pip install -r requirements-dev.txt @@ -150,29 +186,42 @@ jobs: bash <(curl -s https://codecov.io/bash) -cF python test-mongodb: - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 strategy: matrix: python-version: [3.7] services: - mssql: + mongo: image: mongo:4.4.1-bionic ports: - 27017:27017 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + - name: Run openldap + run: | + docker run -d \ + -v '${{ github.workspace }}/docker/openldap/ldifs:/ldifs' \ + -v '${{ github.workspace }}/docker/openldap/schemas/memberof.ldif:/opt/bitnami/openldap/etc/schema/memberof.ldif' \ + -e LDAP_URI=ldap://openldap:1389 \ + -e LDAP_BASE=dc=example,dc=org \ + -e LDAP_ADMIN_USERNAME=admin \ + -e LDAP_ADMIN_PASSWORD=admin_password \ + -e LDAP_EXTRA_SCHEMAS=cosine,inetorgperson,nis,memberof \ + -p 1389:1389 \ + bitnami/openldap:2.6.4 - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y python-dev libldap2-dev libsasl2-dev libssl-dev + sudo apt-get install -y libldap2-dev libsasl2-dev libssl-dev pip install --upgrade pip pip install -r requirements.txt pip install -r requirements-dev.txt pip install -r requirements-extra.txt + pip install -r requirements-mongodb.txt - name: Run tests run: | nosetests --stop -v --with-coverage --cover-package=flask_appbuilder flask_appbuilder/tests/test_mongoengine.py diff --git a/docker-compose.yml b/docker-compose.yml index e6b1435425..900058a4ff 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,3 +20,18 @@ services: MONGO_INITDB_DATABASE: app ports: - 27017:27017 + ldap: + container_name: fab-ldap + image: bitnami/openldap:2.6.4 + environment: + LDAP_URI: ldap://openldap:1389 + LDAP_BASE: dc=example,dc=org + LDAP_ADMIN_USERNAME: admin + LDAP_ADMIN_PASSWORD: admin_password + LDAP_CUSTOM_LDIF_DIR: /ldifs + LDAP_EXTRA_SCHEMAS: cosine,inetorgperson,nis,memberof + volumes: + - './docker/openldap/ldifs:/ldifs' + - './docker/openldap/schemas/memberof.ldif:/opt/bitnami/openldap/etc/schema/memberof.ldif' + ports: + - 1389:1389 diff --git a/docker/openldap/ldifs/users.ldif b/docker/openldap/ldifs/users.ldif new file mode 100644 index 0000000000..e23dad564b --- /dev/null +++ b/docker/openldap/ldifs/users.ldif @@ -0,0 +1,68 @@ +# extended LDIF +# +# LDAPv3 +# base with scope subtree +# filter: (objectclass=*) +# requesting: ALL +# + +# example.org +dn: dc=example,dc=org +objectClass: dcObject +objectClass: organization +dc: example +o: example + +# users, example.org +dn: ou=users,dc=example,dc=org +objectClass: organizationalUnit +ou: users + +# users, example.org +dn: ou=groups,dc=example,dc=org +objectClass: organizationalUnit +ou: groups + +# alice, users, example.org +dn: cn=alice,ou=users,dc=example,dc=org +cn: alice +sn: Doe +givenName: Alice +objectClass: inetOrgPerson +objectClass: posixAccount +objectClass: shadowAccount +userPassword: alice_password +uid: alice +uidNumber: 1000 +gidNumber: 1000 +mail: alice@example.org +homeDirectory: /home/alice + +# natalie, users, example.org +dn: cn=natalie,ou=users,dc=example,dc=org +cn: natalie +sn: Smith +givenName: Natalie +objectClass: inetOrgPerson +objectClass: posixAccount +objectClass: shadowAccount +userPassword: natalie_password +uid: natalie +uidNumber: 1001 +gidNumber: 1001 +mail: natalie@example.org +homeDirectory: /home/natalie + +# readers, users, example.org +dn: cn=readers,ou=groups,dc=example,dc=org +cn: readers +objectClass: groupOfNames +objectClass: top +member: cn=alice,ou=users,dc=example,dc=org +member: cn=natalie,ou=users,dc=example,dc=org + +dn: cn=staff,ou=groups,dc=example,dc=org +cn: staff +objectClass: groupOfNames +objectClass: top +member: cn=alice,ou=users,dc=example,dc=org diff --git a/docker/openldap/schemas/memberof.ldif b/docker/openldap/schemas/memberof.ldif new file mode 100644 index 0000000000..f3274008b4 --- /dev/null +++ b/docker/openldap/schemas/memberof.ldif @@ -0,0 +1,19 @@ +dn: cn=module,cn=config +cn: module +objectClass: olcModuleList +olcModulePath: /opt/bitnami/openldap/lib/openldap +olcModuleLoad: memberof.so +olcModuleLoad: refint.so + +dn: olcOverlay=memberof,olcDatabase={2}mdb,cn=config +objectClass: olcMemberOf +objectClass: olcOverlayConfig +olcOverlay: memberof + +dn: olcOverlay=refint,olcDatabase={2}mdb,cn=config +objectClass: olcConfig +objectClass: olcOverlayConfig +objectClass: olcRefintConfig +objectClass: top +olcOverlay: refint +olcRefintAttribute: memberof member manager owner \ No newline at end of file diff --git a/flask_appbuilder/models/sqla/filters.py b/flask_appbuilder/models/sqla/filters.py index 3848683cc9..bf7c9a1ba3 100644 --- a/flask_appbuilder/models/sqla/filters.py +++ b/flask_appbuilder/models/sqla/filters.py @@ -34,13 +34,13 @@ def get_field_setup_query(query, model, column_name): """ - Help function for SQLA filters, checks for dot notation on column names. - If it exists, will join the query with the model - from the first part of the field name. + Help function for SQLA filters, checks for dot notation on column names. + If it exists, will join the query with the model + from the first part of the field name. - example: - Contact.created_by: if created_by is a User model, - it will be joined to the query. + example: + Contact.created_by: if created_by is a User model, + it will be joined to the query. """ if not hasattr(model, column_name): # it's an inner obj attr @@ -195,7 +195,11 @@ def apply(self, query, value): logging.warning( "Filter exception for %s with value %s, will not apply", field, value ) - self.datamodel.session.rollback() + try: + self.datamodel.session.rollback() + except SQLAlchemyError: + # on MSSQL a rollback would fail here + pass raise ApplyFilterException(exception=exc) return query.filter(field == rel_obj) @@ -212,7 +216,11 @@ def apply(self, query, value): logging.warning( "Filter exception for %s with value %s, will not apply", field, value ) - self.datamodel.session.rollback() + try: + self.datamodel.session.rollback() + except SQLAlchemyError: + # on MSSQL a rollback would fail here + pass raise ApplyFilterException(exception=exc) return query.filter(field != rel_obj) @@ -234,7 +242,11 @@ def apply_item(self, query, field, value_item): field, value_item, ) - self.datamodel.session.rollback() + try: + self.datamodel.session.rollback() + except SQLAlchemyError: + # on MSSQL a rollback would fail here + pass raise ApplyFilterException(exception=exc) if rel_obj: @@ -279,8 +291,8 @@ def apply(self, query, func): class SQLAFilterConverter(BaseFilterConverter): """ - Class for converting columns into a supported list of filters - specific for SQLAlchemy. + Class for converting columns into a supported list of filters + specific for SQLAlchemy. """ diff --git a/flask_appbuilder/tests/security/test_auth_ldap.py b/flask_appbuilder/tests/security/test_auth_ldap.py index 28f934ffc1..1152b86637 100644 --- a/flask_appbuilder/tests/security/test_auth_ldap.py +++ b/flask_appbuilder/tests/security/test_auth_ldap.py @@ -1,16 +1,17 @@ import logging import os +from typing import List import unittest -from unittest.mock import Mock, patch +from unittest.mock import Mock from flask import Flask from flask_appbuilder import AppBuilder, SQLA from flask_appbuilder.security.manager import AUTH_LDAP +from flask_appbuilder.security.sqla.models import User +from flask_appbuilder.tests.const import USERNAME_ADMIN, USERNAME_READONLY import jinja2 import ldap -from mockldap import MockLdap -from ..const import USERNAME_ADMIN, USERNAME_READONLY logging.basicConfig(format="%(asctime)s:%(levelname)s:%(name)s:%(message)s") logging.getLogger().setLevel(logging.DEBUG) @@ -18,19 +19,8 @@ class LDAPSearchTestCase(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.mockldap = MockLdap(cls.directory) - - @classmethod - def tearDownClass(cls): - del cls.mockldap - def setUp(self): # start MockLdap - self.mockldap.start() - self.ldapobj = self.mockldap["ldap://localhost/"] - # start Flask self.app = Flask(__name__) self.app.jinja_env.undefined = jinja2.StrictUndefined @@ -39,11 +29,11 @@ def setUp(self): ) self.app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False self.app.config["AUTH_TYPE"] = AUTH_LDAP - self.app.config["AUTH_LDAP_SERVER"] = "ldap://localhost/" + self.app.config["AUTH_LDAP_SERVER"] = "ldap://localhost:1389/" self.app.config["AUTH_LDAP_UID_FIELD"] = "uid" self.app.config["AUTH_LDAP_FIRSTNAME_FIELD"] = "givenName" self.app.config["AUTH_LDAP_LASTNAME_FIELD"] = "sn" - self.app.config["AUTH_LDAP_EMAIL_FIELD"] = "email" + self.app.config["AUTH_LDAP_EMAIL_FIELD"] = "mail" # start Database self.db = SQLA(self.app) @@ -59,10 +49,6 @@ def tearDown(self): self.db.session.delete(user_natalie) self.db.session.commit() - # stop MockLdap - self.mockldap.stop() - del self.ldapobj - # stop Flask self.app = None @@ -78,125 +64,9 @@ def assertOnlyDefaultUsers(self): user_names = [user.username for user in users] self.assertEqual(user_names, [USERNAME_ADMIN, USERNAME_READONLY]) - # ---------------- - # LDAP Directory - # ---------------- - top = ("o=test", {"o": ["test"]}) - ou_users = ("ou=users,o=test", {"ou": ["users"]}) - ou_groups = ("ou=groups,o=test", {"ou": ["groups"]}) - user_admin = ( - "uid=admin,ou=users,o=test", - {"uid": ["admin"], "userPassword": ["admin_password"]}, - ) - user_alice = ( - "uid=alice,ou=users,o=test", - { - "uid": ["alice"], - "userPassword": ["alice_password"], - "memberOf": [b"cn=staff,ou=groups,o=test"], - "givenName": [b"Alice"], - "sn": [b"Doe"], - "email": [b"alice@example.com"], - }, - ) - user_natalie = ( - "uid=natalie,ou=users,o=test", - { - "uid": ["natalie"], - "userPassword": ["natalie_password"], - "memberOf": [ - b"cn=staff,ou=groups,o=test", - b"cn=admin,ou=groups,o=test", - b"cn=exec,ou=groups,o=test", - ], - "givenName": [b"Natalie"], - "sn": [b"Smith"], - "email": [b"natalie@example.com"], - }, - ) - group_admins = ( - "cn=admins,ou=groups,o=test", - {"cn": ["admins"], "member": [user_admin[0]]}, - ) - group_staff = ( - "cn=staff,ou=groups,o=test", - {"cn": ["staff"], "member": [user_alice[0]]}, - ) - directory = dict( - [ - top, - ou_users, - ou_groups, - user_admin, - user_alice, - user_natalie, - group_admins, - group_staff, - ] - ) - - # ---------------- - # LDAP Queries - # ---------------- - call_initialize = ("initialize", tuple(["ldap://localhost/"]), {}) - - call_set_option = ("set_option", tuple([ldap.OPT_REFERRALS, 0]), {}) - call_bind_admin = ( - "simple_bind_s", - tuple(["uid=admin,ou=users,o=test", "admin_password"]), - {}, - ) - call_bind_alice = ( - "simple_bind_s", - tuple(["uid=alice,ou=users,o=test", "alice_password"]), - {}, - ) - call_bind_natalie = ( - "simple_bind_s", - tuple(["uid=natalie,ou=users,o=test", "natalie_password"]), - {}, - ) - call_search_alice = ( - "search_s", - tuple(["ou=users,o=test", 2, "(uid=alice)", ["givenName", "sn", "email"]]), - {}, - ) - call_search_alice_memberof = ( - "search_s", - tuple( - [ - "ou=users,o=test", - 2, - "(uid=alice)", - ["givenName", "sn", "email", "memberOf"], - ] - ), - {}, - ) - call_search_natalie_memberof = ( - "search_s", - tuple( - [ - "ou=users,o=test", - 2, - "(uid=natalie)", - ["givenName", "sn", "email", "memberOf"], - ] - ), - {}, - ) - call_search_alice_filter = ( - "search_s", - tuple( - [ - "ou=users,o=test", - 2, - "(&(memberOf=cn=staff,ou=groups,o=test)(uid=alice))", - ["givenName", "sn", "email"], - ] - ), - {}, - ) + def assertUserContainsRoles(self, user: User, role_names: List[str]): + user_role_names = sorted([role.name for role in user.roles]) + self.assertListEqual(user_role_names, sorted(role_names)) # ---------------- # Unit Tests @@ -205,30 +75,24 @@ def test___search_ldap(self): """ LDAP: test `_search_ldap` method """ - self.app.config["AUTH_LDAP_BIND_USER"] = "uid=admin,ou=users,o=test" + self.app.config["AUTH_LDAP_BIND_USER"] = "cn=admin,dc=example,dc=org" self.app.config["AUTH_LDAP_BIND_PASSWORD"] = "admin_password" - self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,o=test" + self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org" self.appbuilder = AppBuilder(self.app, self.db.session) sm = self.appbuilder.sm # prepare `con` object - con = ldap.initialize("ldap://localhost/") + con = ldap.initialize("ldap://localhost:1389/") sm._ldap_bind_indirect(ldap, con) # run `_search_ldap` method user_dn, user_attributes = sm._search_ldap(ldap, con, "alice") # validate - search returned expected data - self.assertEqual(user_dn, self.user_alice[0]) - self.assertEqual(user_attributes["givenName"], self.user_alice[1]["givenName"]) - self.assertEqual(user_attributes["sn"], self.user_alice[1]["sn"]) - self.assertEqual(user_attributes["email"], self.user_alice[1]["email"]) - - # validate - expected LDAP methods were called - self.assertEqual( - self.ldapobj.methods_called(with_args=True), - [self.call_initialize, self.call_bind_admin, self.call_search_alice], - ) + self.assertEqual(user_dn, "cn=alice,ou=users,dc=example,dc=org") + self.assertEqual(user_attributes["givenName"], [b"Alice"]) + self.assertEqual(user_attributes["sn"], [b"Doe"]) + self.assertEqual(user_attributes["mail"], [b"alice@example.org"]) def test___search_ldap_filter(self): """ @@ -236,60 +100,50 @@ def test___search_ldap_filter(self): """ # MockLdap needs non-bytes for search filters, so we patch `memberOf` # to a string, only for this test - with patch.dict( - self.directory[self.user_alice[0]], - { - "memberOf": [ - i.decode() for i in self.directory[self.user_alice[0]]["memberOf"] - ] - }, - ): - _mockldap = MockLdap(self.directory) - _mockldap.start() - _ldapobj = _mockldap["ldap://localhost/"] - - self.app.config["AUTH_LDAP_BIND_USER"] = "uid=admin,ou=users,o=test" - self.app.config["AUTH_LDAP_BIND_PASSWORD"] = "admin_password" - self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,o=test" - self.app.config[ - "AUTH_LDAP_SEARCH_FILTER" - ] = "(memberOf=cn=staff,ou=groups,o=test)" - self.appbuilder = AppBuilder(self.app, self.db.session) - sm = self.appbuilder.sm - - # prepare `con` object - con = ldap.initialize("ldap://localhost/") - sm._ldap_bind_indirect(ldap, con) - - # run `_search_ldap` method - user_dn, user_info = sm._search_ldap(ldap, con, "alice") - - # validate - search returned expected data - self.assertEqual(user_dn, self.user_alice[0]) - self.assertEqual(user_info["givenName"], self.user_alice[1]["givenName"]) - self.assertEqual(user_info["sn"], self.user_alice[1]["sn"]) - self.assertEqual(user_info["email"], self.user_alice[1]["email"]) - - # validate - expected LDAP methods were called - self.assertEqual( - _ldapobj.methods_called(with_args=True), - [ - self.call_initialize, - self.call_bind_admin, - self.call_search_alice_filter, - ], - ) + + self.app.config["AUTH_LDAP_BIND_USER"] = "cn=admin,dc=example,dc=org" + self.app.config["AUTH_LDAP_BIND_PASSWORD"] = "admin_password" + self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org" + self.app.config[ + "AUTH_LDAP_SEARCH_FILTER" + ] = "(memberOf=cn=staff,ou=groups,dc=example,dc=org)" + self.appbuilder = AppBuilder(self.app, self.db.session) + sm = self.appbuilder.sm + + # prepare `con` object + con = ldap.initialize("ldap://localhost:1389/") + sm._ldap_bind_indirect(ldap, con) + + # run `_search_ldap` method + user_dn, user_attributes = sm._search_ldap(ldap, con, "alice") + + # validate - search returned expected data + self.assertEqual(user_dn, "cn=alice,ou=users,dc=example,dc=org") + self.assertEqual(user_attributes["givenName"], [b"Alice"]) + self.assertEqual(user_attributes["sn"], [b"Doe"]) + self.assertEqual(user_attributes["mail"], [b"alice@example.org"]) def test___search_ldap_with_search_referrals(self): """ LDAP: test `_search_ldap` method w/returned search referrals """ - self.app.config["AUTH_LDAP_BIND_USER"] = "uid=admin,ou=users,o=test" + self.app.config["AUTH_LDAP_BIND_USER"] = "uid=admin,ou=users,dc=example,dc=org" self.app.config["AUTH_LDAP_BIND_PASSWORD"] = "admin_password" - self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,o=test" + self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org" self.appbuilder = AppBuilder(self.app, self.db.session) sm = self.appbuilder.sm + user_alice = ( + "cn=alice,ou=users,dc=example,dc=org", + { + "uid": ["alice"], + "userPassword": ["alice_password"], + "memberOf": [b"cn=staff,ou=groups,o=test"], + "givenName": [b"Alice"], + "sn": [b"Doe"], + "mail": [b"alice@example.org"], + }, + ) # run `_search_ldap` method w/mocked ldap connection mock_con = Mock() mock_con.search_s.return_value = [ @@ -300,16 +154,16 @@ def test___search_ldap_with_search_referrals(self): "DC=ForestDnsZones,DC=mycompany,DC=com" ], ), - self.user_alice, + user_alice, (None, ["ldap://mycompany.com/CN=Configuration,DC=mycompany,DC=com"]), ] user_dn, user_attributes = sm._search_ldap(ldap, mock_con, "alice") # validate - search returned expected data - self.assertEqual(user_dn, self.user_alice[0]) - self.assertEqual(user_attributes["givenName"], self.user_alice[1]["givenName"]) - self.assertEqual(user_attributes["sn"], self.user_alice[1]["sn"]) - self.assertEqual(user_attributes["email"], self.user_alice[1]["email"]) + self.assertEqual(user_dn, user_alice[0]) + self.assertEqual(user_attributes["givenName"], user_alice[1]["givenName"]) + self.assertEqual(user_attributes["sn"], user_alice[1]["sn"]) + self.assertEqual(user_attributes["mail"], user_alice[1]["mail"]) mock_con.search_s.assert_called() @@ -340,13 +194,49 @@ def test__missing_credentials(self): # validate - no users were created self.assertOnlyDefaultUsers() - # validate - expected LDAP methods were called - self.assertEqual(self.ldapobj.methods_called(with_args=True), []) + def test__active_user(self): + """ + LDAP: test login flow for - inactive user + """ + self.app.config[ + "AUTH_LDAP_USERNAME_FORMAT" + ] = "cn=%s,ou=users,dc=example,dc=org" + self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org" + self.appbuilder = AppBuilder(self.app, self.db.session) + sm = self.appbuilder.sm + + # validate - no users are registered + self.assertOnlyDefaultUsers() + + # register a user + new_user = sm.add_user( + username="alice", + first_name="Alice", + last_name="Doe", + email="alice@example.com", + role=[], + ) + + # validate - user was registered + self.assertEqual(len(sm.get_all_users()), 3) + + # set user inactive + new_user.active = True + + # attempt login + user = sm.auth_user_ldap("alice", "alice_password") + + # validate - user was not allowed to log in + self.assertIsNotNone(user) def test__inactive_user(self): """ LDAP: test login flow for - inactive user """ + self.app.config[ + "AUTH_LDAP_USERNAME_FORMAT" + ] = "cn=%s,ou=users,dc=example,dc=org" + self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org" self.appbuilder = AppBuilder(self.app, self.db.session) sm = self.appbuilder.sm @@ -374,20 +264,18 @@ def test__inactive_user(self): # validate - user was not allowed to log in self.assertIsNone(user) - # validate - expected LDAP methods were called - self.assertEqual(self.ldapobj.methods_called(with_args=True), []) - def test__multi_group_user_mapping_to_same_role(self): """ LDAP: test login flow for - user in multiple groups mapping to same role """ self.app.config["AUTH_ROLES_MAPPING"] = { - "cn=staff,ou=groups,o=test": ["Admin"], - "cn=admin,ou=groups,o=test": ["Admin", "User"], - "cn=exec,ou=groups,o=test": ["Public"], + "cn=staff,ou=groups,dc=example,dc=org": ["Admin"], + "cn=readers,ou=groups,dc=example,dc=org": ["User"], } - self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,o=test" - self.app.config["AUTH_LDAP_USERNAME_FORMAT"] = "uid=%s,ou=users,o=test" + self.app.config[ + "AUTH_LDAP_USERNAME_FORMAT" + ] = "cn=%s,ou=users,dc=example,dc=org" + self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org" self.app.config["AUTH_USER_REGISTRATION"] = True self.app.config["AUTH_USER_REGISTRATION_ROLE"] = "Public" self.appbuilder = AppBuilder(self.app, self.db.session) @@ -409,33 +297,21 @@ def test__multi_group_user_mapping_to_same_role(self): self.assertEqual(len(sm.get_all_users()), 3) # validate - user was given the correct roles - self.assertListEqual( - user.roles, - [sm.find_role("Admin"), sm.find_role("Public"), sm.find_role("User")], - ) + self.assertUserContainsRoles(user, ["Public", "User"]) # validate - user was given the correct attributes (read from LDAP) self.assertEqual(user.first_name, "Natalie") self.assertEqual(user.last_name, "Smith") - self.assertEqual(user.email, "natalie@example.com") - - # validate - expected LDAP methods were called - self.assertEqual( - self.ldapobj.methods_called(with_args=True), - [ - self.call_initialize, - self.call_set_option, - self.call_bind_natalie, - self.call_search_natalie_memberof, - ], - ) + self.assertEqual(user.email, "natalie@example.org") def test__direct_bind__unregistered(self): """ LDAP: test login flow for - direct bind - unregistered user """ - self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,o=test" - self.app.config["AUTH_LDAP_USERNAME_FORMAT"] = "uid=%s,ou=users,o=test" + self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org" + self.app.config[ + "AUTH_LDAP_USERNAME_FORMAT" + ] = "cn=%s,ou=users,dc=example,dc=org" self.app.config["AUTH_USER_REGISTRATION"] = True self.app.config["AUTH_USER_REGISTRATION_ROLE"] = "Public" self.appbuilder = AppBuilder(self.app, self.db.session) @@ -459,25 +335,16 @@ def test__direct_bind__unregistered(self): # validate - user was given the correct attributes (read from LDAP) self.assertEqual(user.first_name, "Alice") self.assertEqual(user.last_name, "Doe") - self.assertEqual(user.email, "alice@example.com") - - # validate - expected LDAP methods were called - self.assertEqual( - self.ldapobj.methods_called(with_args=True), - [ - self.call_initialize, - self.call_set_option, - self.call_bind_alice, - self.call_search_alice, - ], - ) + self.assertEqual(user.email, "alice@example.org") def test__direct_bind__unregistered__no_self_register(self): """ LDAP: test login flow for - direct bind - unregistered user - no self-registration """ - self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,o=test" - self.app.config["AUTH_LDAP_USERNAME_FORMAT"] = "uid=%s,ou=users,o=test" + self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org" + self.app.config[ + "AUTH_LDAP_USERNAME_FORMAT" + ] = "cn=%s,ou=users,dc=example,dc=org" self.app.config["AUTH_USER_REGISTRATION"] = False self.appbuilder = AppBuilder(self.app, self.db.session) sm = self.appbuilder.sm @@ -494,15 +361,14 @@ def test__direct_bind__unregistered__no_self_register(self): # validate - no users were registered self.assertOnlyDefaultUsers() - # validate - expected LDAP methods were called - self.assertEqual(self.ldapobj.methods_called(with_args=True), []) - def test__direct_bind__unregistered__no_search(self): """ LDAP: test login flow for - direct bind - unregistered user - no ldap search """ self.app.config["AUTH_LDAP_SEARCH"] = None - self.app.config["AUTH_LDAP_USERNAME_FORMAT"] = "uid=%s,ou=users,o=test" + self.app.config[ + "AUTH_LDAP_USERNAME_FORMAT" + ] = "cn=%s,ou=users,dc=example,dc=org" self.app.config["AUTH_USER_REGISTRATION"] = True self.appbuilder = AppBuilder(self.app, self.db.session) sm = self.appbuilder.sm @@ -516,18 +382,14 @@ def test__direct_bind__unregistered__no_search(self): # validate - user was NOT allowed to log in (because registration requires search) self.assertIsNone(user) - # validate - expected LDAP methods were called - self.assertEqual( - self.ldapobj.methods_called(with_args=True), - [self.call_initialize, self.call_set_option, self.call_bind_alice], - ) - def test__direct_bind__registered(self): """ LDAP: test login flow for - direct bind - registered user """ - self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,o=test" - self.app.config["AUTH_LDAP_USERNAME_FORMAT"] = "uid=%s,ou=users,o=test" + self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org" + self.app.config[ + "AUTH_LDAP_USERNAME_FORMAT" + ] = "cn=%s,ou=users,dc=example,dc=org" self.appbuilder = AppBuilder(self.app, self.db.session) sm = self.appbuilder.sm @@ -539,7 +401,7 @@ def test__direct_bind__registered(self): username="alice", first_name="Alice", last_name="Doe", - email="alice@example.com", + email="alice@example.org", role=[], ) @@ -552,23 +414,14 @@ def test__direct_bind__registered(self): # validate - user was allowed to log in self.assertIsInstance(user, sm.user_model) - # validate - expected LDAP methods were called - self.assertEqual( - self.ldapobj.methods_called(with_args=True), - [ - self.call_initialize, - self.call_set_option, - self.call_bind_alice, - self.call_search_alice, - ], - ) - def test__direct_bind__registered__no_search(self): """ LDAP: test login flow for - direct bind - registered user - no ldap search """ self.app.config["AUTH_LDAP_SEARCH"] = None - self.app.config["AUTH_LDAP_USERNAME_FORMAT"] = "uid=%s,ou=users,o=test" + self.app.config[ + "AUTH_LDAP_USERNAME_FORMAT" + ] = "cn=%s,ou=users,dc=example,dc=org" self.appbuilder = AppBuilder(self.app, self.db.session) sm = self.appbuilder.sm @@ -580,7 +433,7 @@ def test__direct_bind__registered__no_search(self): username="alice", first_name="Alice", last_name="Doe", - email="alice@example.com", + email="alice@example.org", role=[], ) @@ -593,18 +446,12 @@ def test__direct_bind__registered__no_search(self): # validate - user was allowed to log in (because they are already registered) self.assertIsInstance(user, sm.user_model) - # validate - expected LDAP methods were called - self.assertEqual( - self.ldapobj.methods_called(with_args=True), - [self.call_initialize, self.call_set_option, self.call_bind_alice], - ) - def test__indirect_bind__unregistered(self): """ LDAP: test login flow for - indirect bind - unregistered user """ - self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,o=test" - self.app.config["AUTH_LDAP_BIND_USER"] = "uid=admin,ou=users,o=test" + self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org" + self.app.config["AUTH_LDAP_BIND_USER"] = "cn=admin,dc=example,dc=org" self.app.config["AUTH_LDAP_BIND_PASSWORD"] = "admin_password" self.app.config["AUTH_USER_REGISTRATION"] = True self.app.config["AUTH_USER_REGISTRATION_ROLE"] = "Public" @@ -629,26 +476,14 @@ def test__indirect_bind__unregistered(self): # validate - user was given the correct attributes (read from LDAP) self.assertEqual(user.first_name, "Alice") self.assertEqual(user.last_name, "Doe") - self.assertEqual(user.email, "alice@example.com") - - # validate - expected LDAP methods were called - self.assertEqual( - self.ldapobj.methods_called(with_args=True), - [ - self.call_initialize, - self.call_set_option, - self.call_bind_admin, - self.call_search_alice, - self.call_bind_alice, - ], - ) + self.assertEqual(user.email, "alice@example.org") def test__indirect_bind__unregistered__no_self_register(self): """ LDAP: test login flow for - indirect bind - unregistered user - no self-registration """ # noqa - self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,o=test" - self.app.config["AUTH_LDAP_BIND_USER"] = "uid=admin,ou=users,o=test" + self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org" + self.app.config["AUTH_LDAP_BIND_USER"] = "cn=admin,dc=example,dc=org" self.app.config["AUTH_LDAP_BIND_PASSWORD"] = "admin_password" self.app.config["AUTH_USER_REGISTRATION"] = False self.appbuilder = AppBuilder(self.app, self.db.session) @@ -666,15 +501,12 @@ def test__indirect_bind__unregistered__no_self_register(self): # validate - no users were registered self.assertOnlyDefaultUsers() - # validate - expected LDAP methods were called - self.assertEqual(self.ldapobj.methods_called(with_args=True), []) - def test__indirect_bind__unregistered__no_search(self): """ LDAP: test login flow for - indirect bind - unregistered user - no ldap search """ self.app.config["AUTH_LDAP_SEARCH"] = None - self.app.config["AUTH_LDAP_BIND_USER"] = "uid=admin,ou=users,o=test" + self.app.config["AUTH_LDAP_BIND_USER"] = "cn=admin,dc=example,dc=org" self.app.config["AUTH_LDAP_BIND_PASSWORD"] = "admin_password" self.app.config["AUTH_USER_REGISTRATION"] = True self.app.config["AUTH_USER_REGISTRATION_ROLE"] = "Public" @@ -691,18 +523,12 @@ def test__indirect_bind__unregistered__no_search(self): # (because indirect bind requires search) self.assertIsNone(user) - # validate - expected LDAP methods were called - self.assertEqual( - self.ldapobj.methods_called(with_args=True), - [self.call_initialize, self.call_set_option, self.call_bind_admin], - ) - def test__indirect_bind__registered(self): """ LDAP: test login flow for - indirect bind - registered user """ - self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,o=test" - self.app.config["AUTH_LDAP_BIND_USER"] = "uid=admin,ou=users,o=test" + self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org" + self.app.config["AUTH_LDAP_BIND_USER"] = "cn=admin,dc=example,dc=org" self.app.config["AUTH_LDAP_BIND_PASSWORD"] = "admin_password" self.appbuilder = AppBuilder(self.app, self.db.session) sm = self.appbuilder.sm @@ -715,7 +541,7 @@ def test__indirect_bind__registered(self): username="alice", first_name="Alice", last_name="Doe", - email="alice@example.com", + email="alice@example.org", role=[], ) @@ -728,24 +554,12 @@ def test__indirect_bind__registered(self): # validate - user was allowed to log in self.assertIsInstance(user, sm.user_model) - # validate - expected LDAP methods were called - self.assertEqual( - self.ldapobj.methods_called(with_args=True), - [ - self.call_initialize, - self.call_set_option, - self.call_bind_admin, - self.call_search_alice, - self.call_bind_alice, - ], - ) - def test__indirect_bind__registered__no_search(self): """ LDAP: test login flow for - indirect bind - registered user - no ldap search """ self.app.config["AUTH_LDAP_SEARCH"] = None - self.app.config["AUTH_LDAP_BIND_USER"] = "uid=admin,ou=users,o=test" + self.app.config["AUTH_LDAP_BIND_USER"] = "cn=admin,dc=example,dc=org" self.app.config["AUTH_LDAP_BIND_PASSWORD"] = "admin_password" self.appbuilder = AppBuilder(self.app, self.db.session) sm = self.appbuilder.sm @@ -758,7 +572,7 @@ def test__indirect_bind__registered__no_search(self): username="alice", first_name="Alice", last_name="Doe", - email="alice@example.com", + email="alice@example.org", role=[], ) @@ -772,22 +586,17 @@ def test__indirect_bind__registered__no_search(self): # (because indirect bind requires search) self.assertIsNone(user) - # validate - expected LDAP methods were called - self.assertEqual( - self.ldapobj.methods_called(with_args=True), - [self.call_initialize, self.call_set_option, self.call_bind_admin], - ) - def test__direct_bind__unregistered__single_role(self): """ LDAP: test login flow for - direct bind - unregistered user - single role mapping """ self.app.config["AUTH_ROLES_MAPPING"] = { - "cn=staff,ou=groups,o=test": ["User"], - "cn=admins,ou=groups,o=test": ["Admin"], + "cn=staff,ou=groups,dc=example,dc=org": ["Admin"] } - self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,o=test" - self.app.config["AUTH_LDAP_USERNAME_FORMAT"] = "uid=%s,ou=users,o=test" + self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org" + self.app.config[ + "AUTH_LDAP_USERNAME_FORMAT" + ] = "cn=%s,ou=users,dc=example,dc=org" self.app.config["AUTH_USER_REGISTRATION"] = True self.app.config["AUTH_USER_REGISTRATION_ROLE"] = "Public" self.appbuilder = AppBuilder(self.app, self.db.session) @@ -809,33 +618,24 @@ def test__direct_bind__unregistered__single_role(self): self.assertEqual(len(sm.get_all_users()), 3) # validate - user was given the correct roles - self.assertListEqual(user.roles, [sm.find_role("Public"), sm.find_role("User")]) + self.assertUserContainsRoles(user, ["Admin", "Public"]) # validate - user was given the correct attributes (read from LDAP) self.assertEqual(user.first_name, "Alice") self.assertEqual(user.last_name, "Doe") - self.assertEqual(user.email, "alice@example.com") - - # validate - expected LDAP methods were called - self.assertEqual( - self.ldapobj.methods_called(with_args=True), - [ - self.call_initialize, - self.call_set_option, - self.call_bind_alice, - self.call_search_alice_memberof, - ], - ) + self.assertEqual(user.email, "alice@example.org") def test__direct_bind__unregistered__multi_role(self): """ LDAP: test login flow for - direct bind - unregistered user - multi role mapping """ self.app.config["AUTH_ROLES_MAPPING"] = { - "cn=staff,ou=groups,o=test": ["Admin", "User"] + "cn=staff,ou=groups,dc=example,dc=org": ["Admin", "User"] } - self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,o=test" - self.app.config["AUTH_LDAP_USERNAME_FORMAT"] = "uid=%s,ou=users,o=test" + self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org" + self.app.config[ + "AUTH_LDAP_USERNAME_FORMAT" + ] = "cn=%s,ou=users,dc=example,dc=org" self.app.config["AUTH_USER_REGISTRATION"] = True self.app.config["AUTH_USER_REGISTRATION_ROLE"] = "Public" self.appbuilder = AppBuilder(self.app, self.db.session) @@ -857,37 +657,25 @@ def test__direct_bind__unregistered__multi_role(self): self.assertEqual(len(sm.get_all_users()), 3) # validate - user was given the correct roles - self.assertListEqual( - user.roles, - [sm.find_role("Admin"), sm.find_role("Public"), sm.find_role("User")], - ) + self.assertUserContainsRoles(user, ["Admin", "Public", "User"]) # validate - user was given the correct attributes (read from LDAP) self.assertEqual(user.first_name, "Alice") self.assertEqual(user.last_name, "Doe") - self.assertEqual(user.email, "alice@example.com") - - # validate - expected LDAP methods were called - self.assertEqual( - self.ldapobj.methods_called(with_args=True), - [ - self.call_initialize, - self.call_set_option, - self.call_bind_alice, - self.call_search_alice_memberof, - ], - ) + self.assertEqual(user.email, "alice@example.org") def test__direct_bind__registered__multi_role__no_role_sync(self): """ LDAP: test login flow for - direct bind - registered user - multi role mapping - no login role-sync """ # noqa self.app.config["AUTH_ROLES_MAPPING"] = { - "cn=staff,ou=groups,o=test": ["Admin", "User"] + "cn=staff,ou=groups,dc=example,dc=org": ["Admin", "User"] } self.app.config["AUTH_ROLES_SYNC_AT_LOGIN"] = False - self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,o=test" - self.app.config["AUTH_LDAP_USERNAME_FORMAT"] = "uid=%s,ou=users,o=test" + self.app.config[ + "AUTH_LDAP_USERNAME_FORMAT" + ] = "cn=%s,ou=users,dc=example,dc=org" + self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org" self.appbuilder = AppBuilder(self.app, self.db.session) sm = self.appbuilder.sm @@ -902,7 +690,7 @@ def test__direct_bind__registered__multi_role__no_role_sync(self): username="alice", first_name="Alice", last_name="Doe", - email="alice@example.com", + email="alice@example.org", role=[], ) @@ -918,27 +706,18 @@ def test__direct_bind__registered__multi_role__no_role_sync(self): # validate - user was given no roles self.assertListEqual(user.roles, []) - # validate - expected LDAP methods were called - self.assertEqual( - self.ldapobj.methods_called(with_args=True), - [ - self.call_initialize, - self.call_set_option, - self.call_bind_alice, - self.call_search_alice_memberof, - ], - ) - def test__direct_bind__registered__multi_role__with_role_sync(self): """ LDAP: test login flow for - direct bind - registered user - multi role mapping - with login role-sync """ # noqa self.app.config["AUTH_ROLES_MAPPING"] = { - "cn=staff,ou=groups,o=test": ["Admin", "User"] + "cn=staff,ou=groups,dc=example,dc=org": ["Admin", "User"] } self.app.config["AUTH_ROLES_SYNC_AT_LOGIN"] = True - self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,o=test" - self.app.config["AUTH_LDAP_USERNAME_FORMAT"] = "uid=%s,ou=users,o=test" + self.app.config[ + "AUTH_LDAP_USERNAME_FORMAT" + ] = "cn=%s,ou=users,dc=example,dc=org" + self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org" self.appbuilder = AppBuilder(self.app, self.db.session) sm = self.appbuilder.sm @@ -953,7 +732,7 @@ def test__direct_bind__registered__multi_role__with_role_sync(self): username="alice", first_name="Alice", last_name="Doe", - email="alice@example.com", + email="alice@example.org", role=[], ) @@ -967,26 +746,17 @@ def test__direct_bind__registered__multi_role__with_role_sync(self): self.assertIsInstance(user, sm.user_model) # validate - user was given the correct roles - self.assertListEqual(user.roles, [sm.find_role("Admin"), sm.find_role("User")]) - - # validate - expected LDAP methods were called - self.assertEqual( - self.ldapobj.methods_called(with_args=True), - [ - self.call_initialize, - self.call_set_option, - self.call_bind_alice, - self.call_search_alice_memberof, - ], - ) + self.assertUserContainsRoles(user, ["Admin", "User"]) def test__indirect_bind__unregistered__single_role(self): """ LDAP: test login flow for - indirect bind - unregistered user - single role mapping """ # noqa - self.app.config["AUTH_ROLES_MAPPING"] = {"cn=staff,ou=groups,o=test": ["User"]} - self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,o=test" - self.app.config["AUTH_LDAP_BIND_USER"] = "uid=admin,ou=users,o=test" + self.app.config["AUTH_ROLES_MAPPING"] = { + "cn=staff,ou=groups,dc=example,dc=org": ["User"] + } + self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org" + self.app.config["AUTH_LDAP_BIND_USER"] = "cn=admin,dc=example,dc=org" self.app.config["AUTH_LDAP_BIND_PASSWORD"] = "admin_password" self.app.config["AUTH_USER_REGISTRATION"] = True self.app.config["AUTH_USER_REGISTRATION_ROLE"] = "Public" @@ -1009,34 +779,22 @@ def test__indirect_bind__unregistered__single_role(self): self.assertEqual(len(sm.get_all_users()), 3) # validate - user was given the correct roles - self.assertListEqual(user.roles, [sm.find_role("Public"), sm.find_role("User")]) + self.assertUserContainsRoles(user, ["Public", "User"]) # validate - user was given the correct attributes (read from LDAP) self.assertEqual(user.first_name, "Alice") self.assertEqual(user.last_name, "Doe") - self.assertEqual(user.email, "alice@example.com") - - # validate - expected LDAP methods were called - self.assertEqual( - self.ldapobj.methods_called(with_args=True), - [ - self.call_initialize, - self.call_set_option, - self.call_bind_admin, - self.call_search_alice_memberof, - self.call_bind_alice, - ], - ) + self.assertEqual(user.email, "alice@example.org") def test__indirect_bind__unregistered__multi_role(self): """ LDAP: test login flow for - indirect bind - unregistered user - multi role mapping """ self.app.config["AUTH_ROLES_MAPPING"] = { - "cn=staff,ou=groups,o=test": ["Admin", "User"] + "cn=staff,ou=groups,dc=example,dc=org": ["Admin", "User"] } - self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,o=test" - self.app.config["AUTH_LDAP_BIND_USER"] = "uid=admin,ou=users,o=test" + self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org" + self.app.config["AUTH_LDAP_BIND_USER"] = "cn=admin,dc=example,dc=org" self.app.config["AUTH_LDAP_BIND_PASSWORD"] = "admin_password" self.app.config["AUTH_USER_REGISTRATION"] = True self.app.config["AUTH_USER_REGISTRATION_ROLE"] = "Public" @@ -1059,38 +817,23 @@ def test__indirect_bind__unregistered__multi_role(self): self.assertEqual(len(sm.get_all_users()), 3) # validate - user was given the correct roles - self.assertListEqual( - user.roles, - [sm.find_role("Admin"), sm.find_role("Public"), sm.find_role("User")], - ) + self.assertUserContainsRoles(user, ["User", "Public", "Admin"]) # validate - user was given the correct attributes (read from LDAP) self.assertEqual(user.first_name, "Alice") self.assertEqual(user.last_name, "Doe") - self.assertEqual(user.email, "alice@example.com") - - # validate - expected LDAP methods were called - self.assertEqual( - self.ldapobj.methods_called(with_args=True), - [ - self.call_initialize, - self.call_set_option, - self.call_bind_admin, - self.call_search_alice_memberof, - self.call_bind_alice, - ], - ) + self.assertEqual(user.email, "alice@example.org") def test__indirect_bind__registered__multi_role__no_role_sync(self): """ LDAP: test login flow for - indirect bind - registered user - multi role mapping - no login role-sync """ # noqa self.app.config["AUTH_ROLES_MAPPING"] = { - "cn=staff,ou=groups,o=test": ["Admin", "User"] + "cn=staff,ou=groups,dc=example,dc=org": ["Admin", "User"] } self.app.config["AUTH_ROLES_SYNC_AT_LOGIN"] = False - self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,o=test" - self.app.config["AUTH_LDAP_BIND_USER"] = "uid=admin,ou=users,o=test" + self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org" + self.app.config["AUTH_LDAP_BIND_USER"] = "cn=admin,dc=example,dc=org" self.app.config["AUTH_LDAP_BIND_PASSWORD"] = "admin_password" self.appbuilder = AppBuilder(self.app, self.db.session) sm = self.appbuilder.sm @@ -1106,7 +849,7 @@ def test__indirect_bind__registered__multi_role__no_role_sync(self): username="alice", first_name="Alice", last_name="Doe", - email="alice@example.com", + email="alice@example.org", role=[], ) @@ -1122,28 +865,16 @@ def test__indirect_bind__registered__multi_role__no_role_sync(self): # validate - user was given no roles self.assertListEqual(user.roles, []) - # validate - expected LDAP methods were called - self.assertEqual( - self.ldapobj.methods_called(with_args=True), - [ - self.call_initialize, - self.call_set_option, - self.call_bind_admin, - self.call_search_alice_memberof, - self.call_bind_alice, - ], - ) - def test__indirect_bind__registered__multi_role__with_role_sync(self): """ LDAP: test login flow for - indirect bind - registered user - multi role mapping - with login role-sync """ # noqa self.app.config["AUTH_ROLES_MAPPING"] = { - "cn=staff,ou=groups,o=test": ["Admin", "User"] + "cn=staff,ou=groups,dc=example,dc=org": ["Admin", "User"] } self.app.config["AUTH_ROLES_SYNC_AT_LOGIN"] = True - self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,o=test" - self.app.config["AUTH_LDAP_BIND_USER"] = "uid=admin,ou=users,o=test" + self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org" + self.app.config["AUTH_LDAP_BIND_USER"] = "cn=admin,dc=example,dc=org" self.app.config["AUTH_LDAP_BIND_PASSWORD"] = "admin_password" self.appbuilder = AppBuilder(self.app, self.db.session) sm = self.appbuilder.sm @@ -1159,7 +890,7 @@ def test__indirect_bind__registered__multi_role__with_role_sync(self): username="alice", first_name="Alice", last_name="Doe", - email="alice@example.com", + email="alice@example.org", role=[], ) @@ -1173,27 +904,16 @@ def test__indirect_bind__registered__multi_role__with_role_sync(self): self.assertIsInstance(user, sm.user_model) # validate - user was given the correct roles - self.assertListEqual(user.roles, [sm.find_role("Admin"), sm.find_role("User")]) - - # validate - expected LDAP methods were called - self.assertEqual( - self.ldapobj.methods_called(with_args=True), - [ - self.call_initialize, - self.call_set_option, - self.call_bind_admin, - self.call_search_alice_memberof, - self.call_bind_alice, - ], - ) + self.assertUserContainsRoles(user, ["User", "Admin"]) def test_login_failed_keep_next_url(self): """ LDAP: Keeping next url after failed login attempt """ - - self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,o=test" - self.app.config["AUTH_LDAP_USERNAME_FORMAT"] = "uid=%s,ou=users,o=test" + self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org" + self.app.config[ + "AUTH_LDAP_USERNAME_FORMAT" + ] = "cn=%s,ou=users,dc=example,dc=org" self.app.config["AUTH_USER_REGISTRATION"] = True self.app.config["AUTH_USER_REGISTRATION_ROLE"] = "Public" self.app.config["WTF_CSRF_ENABLED"] = False diff --git a/requirements-dev.txt b/requirements-dev.txt index 9bba01d1bf..091e877fa2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,7 +6,6 @@ hiro==0.5.1 jmespath==0.9.5 mypy==0.910 mypy-extensions==0.4.3 -mockldap>=0.3.0 nose==1.3.7 parameterized==0.8.1 pip-tools==6.8.0 diff --git a/requirements-extra.txt b/requirements-extra.txt index d9b6ebdd3d..c6ddb42e4f 100644 --- a/requirements-extra.txt +++ b/requirements-extra.txt @@ -1,10 +1,7 @@ -mongoengine>=0.7.10, <0.7.99 -flask-mongoengine==0.7.1 -pymongo>=2.8.1, <2.8.99 Pillow~=9.1 cython==0.29.17 mysqlclient==2.0.1 -psycopg2-binary==2.8.6 +psycopg2-binary==2.9.6 pyodbc==4.0.35 requests==2.26.0 Authlib==0.15.4 diff --git a/requirements-mongodb.txt b/requirements-mongodb.txt new file mode 100644 index 0000000000..07e58f7783 --- /dev/null +++ b/requirements-mongodb.txt @@ -0,0 +1,3 @@ +flask-mongoengine==0.9.5 +mongoengine==0.17.0 +pymongo==3.4.0 diff --git a/requirements.txt b/requirements.txt index 7394aa7907..bf44afb380 100644 --- a/requirements.txt +++ b/requirements.txt @@ -82,7 +82,7 @@ prison==0.2.1 # via Flask-AppBuilder (setup.py) pygments==2.13.0 # via rich -pyjwt==2.3.0 +pyjwt==2.6.0 # via # Flask-AppBuilder (setup.py) # flask-jwt-extended diff --git a/rtd_requirements.txt b/rtd_requirements.txt index e7901b763e..80f5dc67d3 100644 --- a/rtd_requirements.txt +++ b/rtd_requirements.txt @@ -1,4 +1,3 @@ -funcparserlib==1.0.0.a0 # required by: https://github.com/vlasovskikh/funcparserlib/issues/70 sphinxcontrib-blockdiag>=1.5.0 sphinx-rtd-theme>=0.2.4 apispec[yaml]==3.3.2