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]: