apache / superset

Apache Superset is a Data Visualization and Data Exploration Platform
https://superset.apache.org/
Apache License 2.0
61.94k stars 13.57k forks source link

New Feature: Decide Superset Role Automatically based on certain LDAP field (like department) #3419

Closed Jie-Yang closed 6 years ago

Jie-Yang commented 7 years ago

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

  1. what would be best approach to implement such request?
  2. Is it a good feature which could be included in Superset for future release?

Thanks.

xrmx commented 7 years ago

Authentication is handled by flask app builder, have you checked it doesn't already have hooks?

Jie-Yang commented 7 years ago

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.

Jie-Yang commented 7 years ago

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.

  1. create a new file "mysecurity.py" and place beside superset_config.py.

` 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)

`

  1. in superset_config_py, add following line from mysecurity import MySecurityManager CUSTOM_SECURITY_MANAGER = MySecurityManager
lilloraffa commented 6 years ago

This would be a cool feature to be handled natively by Superset.

mistercrunch commented 6 years ago

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.

metaperl commented 4 years ago

And how did you register each department as a role in Superset? via the UI or programmatically?

Jie-Yang commented 4 years ago

Hi @metaperl I am afraid that you have to do that programmatically.

thesuperzapper commented 4 years ago

I have created a PR which adds this feature to FlaskAppBuilder, see here

parthasil92 commented 1 year ago

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.

a mapping from LDAP DN to a list of FAB roles

AUTH_ROLES_MAPPING = { "cn=fab_users,ou=groups,dc=example,dc=com": ["User"], "cn=fab_admins,ou=groups,dc=example,dc=com": ["Admin"], }

the LDAP user attribute which has their role DNs

AUTH_LDAP_GROUP_FIELD = "memberOf"

if we should replace ALL the user's roles each login, or only on registration

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.