diff --git a/docs/config.rst b/docs/config.rst
index faa060b3ab..bf48e24709 100755
--- a/docs/config.rst
+++ b/docs/config.rst
@@ -209,6 +209,8 @@ Use config.py to configure the following parameters. By default it will use SQLL
| | | |
| | It authenticates with "format-userexample".| |
+----------------------------------------+--------------------------------------------+-----------+
+| AUTH_LDAP_USE_NESTED_GROUPS_FOR_ROLES | Get users nested groups from LDAP(MS AD) | No |
++----------------------------------------+--------------------------------------------+-----------+
| AUTH_ROLE_ADMIN | Configure the name of the admin role. | No |
+----------------------------------------+--------------------------------------------+-----------+
| AUTH_ROLE_PUBLIC | Special Role that holds the public | No |
@@ -333,10 +335,10 @@ It should be a long random bytes or str. For example, copy the output of this to
Using config.py
---------------
-
+
My favorite way, and the one I advise if you are building a medium to large size application
is to place all your configuration keys on a config.py file
-
+
Next you only have to import them to the Flask app object, like this
::
@@ -351,10 +353,10 @@ Take a look at the skeleton `config.py `_ expression to evalate user registration role. The input
values is ``userinfo`` dict, returned by ``get_oauth_user_info`` function of Security Manager.
-Usage of JMESPath expressions requires `jmespath `_ package
+Usage of JMESPath expressions requires `jmespath `_ package
to be installed.
In case of Google OAuth, userinfo contains user's email that can be used to map some users as admins
diff --git a/flask_appbuilder/security/manager.py b/flask_appbuilder/security/manager.py
index 20b35d2644..4a86a5fe2a 100644
--- a/flask_appbuilder/security/manager.py
+++ b/flask_appbuilder/security/manager.py
@@ -257,6 +257,8 @@ def __init__(self, appbuilder):
app.config.setdefault("AUTH_LDAP_FIRSTNAME_FIELD", "givenName")
app.config.setdefault("AUTH_LDAP_LASTNAME_FIELD", "sn")
app.config.setdefault("AUTH_LDAP_EMAIL_FIELD", "mail")
+ # Nested groups options
+ app.config.setdefault("AUTH_LDAP_USE_NESTED_GROUPS_FOR_ROLES", False)
# Rate limiting
app.config.setdefault("AUTH_RATE_LIMITED", False)
@@ -495,6 +497,10 @@ def auth_ldap_tls_certfile(self):
def auth_ldap_tls_keyfile(self):
return self.appbuilder.get_app.config["AUTH_LDAP_TLS_KEYFILE"]
+ @property
+ def auth_ldap_use_nested_groups_for_roles(self):
+ return self.appbuilder.get_app.config["AUTH_LDAP_USE_NESTED_GROUPS_FOR_ROLES"]
+
@property
def openid_providers(self):
return self.appbuilder.get_app.config["OPENID_PROVIDERS"]
@@ -963,11 +969,39 @@ def _search_ldap(self, ldap, con, username):
user_dn = search_result[0][0]
# extract the other attributes
user_info = search_result[0][1]
- # return
- return user_dn, user_info
except (IndexError, NameError):
return None, None
+ # get nested groups for user
+ if self.auth_ldap_use_nested_groups_for_roles:
+ log.debug("Nested groups for LDAP enabled.")
+ # filter for microsoft active directory only
+ nested_groups_filter_str = (
+ f"(&(objectCategory=Group)(member:1.2.840.113556.1.4.1941:={user_dn}))"
+ )
+ nested_groups_request_fields = ["cn"]
+
+ nested_groups_search_result = con.search_s(
+ self.auth_ldap_search,
+ ldap.SCOPE_SUBTREE,
+ nested_groups_filter_str,
+ nested_groups_request_fields,
+ )
+ log.debug(
+ "LDAP search for nested groups returned: %s",
+ nested_groups_search_result,
+ )
+
+ nested_groups = [
+ x[0].encode() for x in nested_groups_search_result if x[0] is not None
+ ]
+ log.debug("LDAP nested groups for users: %s", nested_groups)
+
+ user_info[self.auth_ldap_group_field] = nested_groups
+
+ # return
+ return user_dn, user_info
+
def _ldap_calculate_user_roles(
self, user_attributes: Dict[str, bytes]
) -> List[str]: