Closed Jie-Yang closed 6 years ago
Authentication is handled by flask app builder, have you checked it doesn't already have hooks?
Hi @xrmx , Thanks for the reply. AUTH_USER_REGISTRATION_ROLE is the only config from flask app builder which I can find, but it can only can hook all new user to one single role. If there is other config from flask can do what I want, please let me know. I appreciate of that.
For people who might come across the same requirement, please find my solution below which might save you some time.
I do think that this would be really good feature for Superset to have, since that it would be very difficult to assign different roles to different user groups if there are hundreds of them. My code below is quite messy. And if any people feel the same way, I am more than happy to help to add this feature formally to the Superset source code.
` from flask_appbuilder.security.sqla.manager import SecurityManager import logging, datetime from flask_appbuilder import const as c import re log = logging.getLogger(name)
class MySecurityManager(SecurityManager):
auth_ldap_department_field = 'distinguishedName'
def auth_user_ldap(self, username, password):
if username is None or username == "":
return None
user = self.find_user(username=username)
log.debug('----------->'+str(username))
if user is not None and (not user.is_active()):
return None
else:
try:
import ldap
except:
raise Exception("No ldap library for python.")
try:
if self.auth_ldap_allow_self_signed:
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_ALLOW)
con = ldap.initialize(self.auth_ldap_server)
con.set_option(ldap.OPT_REFERRALS, 0)
if self.auth_ldap_use_tls:
try:
con.start_tls_s()
except Exception:
log.info(c.LOGMSG_ERR_SEC_AUTH_LDAP_TLS.format(self.auth_ldap_server))
return None
# Authenticate user
if not self._bind_ldap(ldap, con, username, password):
if user:
self.update_user_auth_stat(user, False)
log.info(c.LOGMSG_WAR_SEC_LOGIN_FAILED.format(username))
return None
# If user does not exist on the DB and not self user registration, go away
if not user and not self.auth_user_registration:
return None
# User does not exist, create one if self registration.
elif not user and self.auth_user_registration:
new_user = self._search_ldap(ldap, con, username)
if not new_user:
log.warning(c.LOGMSG_WAR_SEC_NOLDAP_OBJ.format(username))
return None
ldap_user_info = new_user[0][1]
if self.auth_user_registration and user is None:
department = self.ldap_extract_department(ldap_user_info)
user = self.add_user(
username=username,
first_name=self.ldap_extract(ldap_user_info, self.auth_ldap_firstname_field, username),
last_name=self.ldap_extract(ldap_user_info, self.auth_ldap_lastname_field, username),
email=self.ldap_extract(ldap_user_info, self.auth_ldap_email_field,
username + '@email.notfound'),
role=self.find_role_by_department(department)
)
log.debug('--new LDAP User->' + str(user))
self.update_user_auth_stat(user)
return user
except ldap.LDAPError as e:
if type(e.message) == dict and 'desc' in e.message:
log.error(c.LOGMSG_ERR_SEC_AUTH_LDAP.format(e.message['desc']))
return None
else:
log.error(e)
return None
def ldap_extract_department(self, ldap_user_info):
department_name = None
try:
department_info = str(ldap_user_info[self.auth_ldap_department_field][0])
department_name = re.findall(r'OU=[^\,\$]+,', department_info)[0].replace('OU=', '')
# lower case, remove all space
department_name = department_name.lower()
# remove all special char
department_name = re.sub('[^A-Za-z0-9]+', '', department_name)
log.debug('find LDAP user department:' + str(department_name))
except Exception as e:
log.warning(c.LOGMSG_ERR_SEC_AUTH_LDAP.format(e))
log.warning(department_info)
return department_name
def find_role_by_department(self, department):
role_name = self.auth_user_registration_role
if department:
# use cleaned department directly as role name
role_name = department
role = self.find_role(role_name)
if not role:
role = self.find_role(self.auth_user_registration_role)
log.debug('can NOT find role:' + str(role_name)+', use default '+str(self.auth_user_registration_role))
log.debug('find role by department:' + str(department) + '-> ' + str(role_name))
return role
def _search_ldap(self, ldap, con, username):
"""
Searches LDAP for user, assumes ldap_search is set.
:param ldap: The ldap module reference
:param con: The ldap connection
:param username: username to match with auth_ldap_uid_field
:return: ldap object array
"""
if self.auth_ldap_append_domain:
username = username + '@' + self.auth_ldap_append_domain
filter_str = "%s=%s" % (self.auth_ldap_uid_field, username)
user = con.search_s(self.auth_ldap_search,
ldap.SCOPE_SUBTREE,
filter_str,
[self.auth_ldap_firstname_field,
self.auth_ldap_lastname_field,
self.auth_ldap_email_field,
self.auth_ldap_department_field
])
if user:
if not user[0][0]:
return None
return user
'''
bug fix: 'not user.login_count' will rise error in python 3
File "/usr/local/lib/python3.6/site-packages/flask_appbuilder/security/manager.py", line 502, in update_user_auth_stat
superset_1 | if not user.login_count:
superset_1 | AttributeError: 'bool' object has no attribute 'login_count'
'''
def update_user_auth_stat(self, user, success=True):
if not hasattr(user, 'login_count'):
user.login_count = 0
if not user.login_count:
user.login_count = 0
if not user.fail_login_count:
user.fail_login_count = 0
if success:
user.login_count += 1
user.fail_login_count = 0
else:
user.fail_login_count += 1
user.last_login = datetime.datetime.now()
self.update_user(user)
`
from mysecurity import MySecurityManager CUSTOM_SECURITY_MANAGER = MySecurityManager
This would be a cool feature to be handled natively by Superset.
Notice: this issue has been closed because it has been inactive for 204 days. Feel free to comment and request for this issue to be reopened.
And how did you register each department as a role in Superset? via the UI or programmatically?
Hi @metaperl I am afraid that you have to do that programmatically.
I have created a PR which adds this feature to FlaskAppBuilder, see here
Hi Everyone. I checked if our LDAP is created in the below ways the dynamic role mapping is working perfectly fine when integrating with superset.
AUTH_ROLES_MAPPING = { "cn=fab_users,ou=groups,dc=example,dc=com": ["User"], "cn=fab_admins,ou=groups,dc=example,dc=com": ["Admin"], }
AUTH_LDAP_GROUP_FIELD = "memberOf"
AUTH_ROLES_SYNC_AT_LOGIN = True
But in my case, I have two OUs say, OU=OU_A and OU=OU_B in LDAP and inside OUs members are there. I want to assign all users in OU_A to Admin role and OU_B to Gamma role respectively. I used AUTH_LDAP_GROUP_FIELD ='ou'. But it is not working. Please help me to resolve this issue.
Superset version
0.18.5
Expected results
When the Superset connect with LDAP server, we would love to see that the user will be given Superset role dynamically and automatically based on his/her records in LDAP server. Particularly speaking, our LDAP server keep records of department where the user is belonged to. Hence, I want to give the user Super role automatically based on what department he/she comes from.
Actual results
No such feature exists as what I can find now. Currently, we can only use the env var below in config file to set one singe role for all users. For example, Gamma role for all new users from LDAP auth.
AUTH_USER_REGISTRATION_ROLE= 'Gamma'
Questions
Thanks.