This is the 17th day of my participation in the August Text Challenge.More challenges in August

Introduction to the

Blue Whale login access In the enterprise internal login through Google login example to illustrate; However, only LDAP provides unified authentication for internal services and does not provide relevant login apis.

The above is probably the situation of many small and medium-sized enterprises. In this case, how to access LDAP?

Don’t worry, let’s look at the source code is how to achieve?

The source code parsing

Let’s analyze the basic function interface of unified login service of Blue Whale PAAS platform to see the login process for our reference.

1. Basic functions provided by Blue Whale Unified Login

from bkaccount.accounts import Account
Copy the code

It can be seen from the above python module import that the login jump function of blue whale is mainly realized by Account class, in which the login page and login action functions are mainly realized by login:

    def login(self, request, template_name='login/login.html',
              authentication_form=AuthenticationForm,
              current_app=None, extra_context=None) :
        "" login page and login action """
        redirect_field_name = self.REDIRECT_FIELD_NAME
        redirect_to = request.POST.get(redirect_field_name,
                                       request.GET.get(redirect_field_name, ' '))
        app_id = request.POST.get('app_id', request.GET.get('app_id'.' '))

        if request.method == 'POST':
            form = authentication_form(request, data=request.POST)
            if form.is_valid():
                return self.login_success_response(request, form, redirect_to, app_id)
        else:
            form = authentication_form(request)

        current_site = get_current_site(request)
        context = {
            'form': form,
            redirect_field_name: redirect_to,
            'site': current_site,
            'site_name': current_site.name,
            'app_id': app_id,
        }
        if extra_context is not None:
            context.update(extra_context)
        if current_app is not None:
            request.current_app = current_app

        response = TemplateResponse(request, template_name, context)
        response = self.set_bk_token_invalid(request, response)
        return response
Copy the code

When the user name and password are entered on the login page, a POST request will be sent, with the following code segment:

        if request.method == 'POST':
            form = authentication_form(request, data=request.POST)
            if form.is_valid():
                return self.login_success_response(request, form, redirect_to, app_id)
        else:
            form = authentication_form(request)
Copy the code

We can see that authentication_form is used for processing at this point, and authentication_form comes from the parameter authentication_form=AuthenticationForm passed in by the login function, AuthenticationForm again from the from the django. Contrib. Auth, forms an import AuthenticationForm, and AuthenticationForm is a form.

2. Login form authentication

AuthenticationForm is a form defined as follows:

class AuthenticationForm(forms.Form) :
    """ Base class for authenticating users. Extend this to get a form that accepts username/password logins. """
    username = forms.CharField(max_length=254)
    password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)

    error_messages = {
        'invalid_login': _ ("Please enter a correct %(username)s and password. "
                           "Note that both fields may be case-sensitive."),
        'inactive': _ ("This account is inactive."),}def __init__(self, request=None, *args, **kwargs) :
        """ The 'request' parameter is set for custom auth use by subclasses. The form data comes in via the standard 'data' kwarg. """
        self.request = request
        self.user_cache = None
        super(AuthenticationForm, self).__init__(*args, **kwargs)

        # Set the label for the "username" field.
        UserModel = get_user_model()
        self.username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)
        if self.fields['username'].label is None:
            self.fields['username'].label = capfirst(self.username_field.verbose_name)

    def clean(self) :
        username = self.cleaned_data.get('username')
        password = self.cleaned_data.get('password')

        if username and password:
            self.user_cache = authenticate(username=username,
                                           password=password)
            if self.user_cache is None:
                raise forms.ValidationError(
                    self.error_messages['invalid_login'],
                    code='invalid_login',
                    params={'username': self.username_field.verbose_name},
                )
            else:
                self.confirm_login_allowed(self.user_cache)

        return self.cleaned_data

    def confirm_login_allowed(self, user) :
        """ Controls whether the given User may log in. This is a policy setting, independent of end-user authentication. This default behavior is to allow login by active users, and reject login by inactive users. If the given user cannot log in, this method should raise a ``forms.ValidationError``. If the given user may log in, this method should return None. """
        if not user.is_active:
            raise forms.ValidationError(
                self.error_messages['inactive'],
                code='inactive'.)def get_user_id(self) :
        if self.user_cache:
            return self.user_cache.id
        return None

    def get_user(self) :
        return self.user_cache
Copy the code

As you can see from django’s forms, the request. Post data is retrieved from the form cleaned_data.get (cleaned_data.get), which is called cleaned_data.get (cleaned_data.get).

    def clean(self) :
        username = self.cleaned_data.get('username')
        password = self.cleaned_data.get('password')

        if username and password:
            self.user_cache = authenticate(username=username,
                                           password=password)
            if self.user_cache is None:
                raise forms.ValidationError(
                    self.error_messages['invalid_login'],
                    code='invalid_login',
                    params={'username': self.username_field.verbose_name},
                )
            else:
                self.confirm_login_allowed(self.user_cache)

        return self.cleaned_data
Copy the code

According to the code, if the user name and password are not empty, authenticate is called. Contrib. auth import authenticate, authenticate is a function that can be overwritten by the user login module: Blue whale login access enterprise internal login introduction is correct.

3. Login summary

Companies without a login API can actually do local authentication by overriding the Clean method of the AuthenticationForm form.

Let’s implement the blue Whale community 5.1 access LDAP authentication.

Blue Whale Community Edition 5.1 Access LDAP authentication

I. Construction of development environment

Maybe our blue whale has already been used in production. In order to avoid affecting the use, we temporarily set up the unified login service of blue whale PAAS platform. Please refer to Tencent Blue Whale Wisdom Cloud/BK-PAas. Blue Whale PAAS platform login (Blue Whale unified login service), PAAS (Blue Whale Developer Center), ESB (Blue Whale API Gateway), AppEngine (Blue Whale application engine), PaASAgent (Blue Whale application engine Agent); The development environment only needs to set up login, which is the unified login service that all services under Blue Whale Intelligence cloud depend on, including operation platform/configuration platform /PaaS platform /SaaS platform, etc.

The deployment process can refer to the official installation and deployment section, which I will briefly introduce

1. Create a database

Create database open_paAS
CREATE DATABASE IF NOT EXISTS open_paas DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
Copy the code

Deploy the Web project

Enter paas VirtualEnv automatically
$ virtualenv login

$ which python
$ cd paas-ce/paas/login/

# install dependencies
$ pip install -r requirements.txt

Modify the configuration file, configure the database, domain name, etc. Notice LOGIN_DOMAIN needs to be configured for local development
$ vim conf/settings_development.py

Log/paas: execute migrate
The login/PAAS projects need to perform migration
python manage.py migrate

Pull up the service to use another managed service, such as Supervisor
$ python manage.py runserver 8003
Copy the code

3. Configuration files

The following two main modifications can be made

# paas
paas/conf/settings_development.py

# login
login/conf/settings_development.py
Copy the code

The above is the construction of the development environment for the early stage of development and debugging.

The following is the official access to Blue Whale Community 5.1.

2. Official access

1. Login function description

1. A common user is authenticated by LDAP. If a user exists in LDAP but does not exist in Blue Whale, a new user is created and set to a common user. 2. Skip LDAP authentication and use blue Whale authentication to log in to the admin user.

Consider: If ldap cannot be connected or the connection fails, you can skip LDAP authentication and use blue Whale authentication. This function is not completed in this development, you can implement.

2. Directory structure

Ee_login / ├─ enterprise_ldap ## Custom Login module Directory │ ├─ Backends. py ## Check user validity │ ├─ _init_.py │ ├─ ldap.py ## Access LDAP and get user information │ ├─ Utils. Py ## Custom form, integrate AuthenticationForm, ├─ ├─ ├─ ├─ ├.py ## Customizing login files

3. Create a module directory and modify the configuration file

# Paas machine
Install the LDAP moduleWorkon open_paas-login PIP install LDap3 Ensure that the open_paas-login virtual environment is used. Otherwise, ldap cannot be found# control machine
cd /data/bkce/open_paas/login/ee_login
Create a custom login module directory
mkdir enterprise_ldap
Modify the configuration file
vim settings_login.py
# -*- coding: utf-8 -*-
Blue Whale login: bk_login
# Customize the login method: custom_login

#LOGIN_TYPE = 'bk_login'
LOGIN_TYPE = 'custom_login'

# Default bk_login, no additional configuration required

# # # # # # # # # # # # # # # # # # # # # # # # # # #
# Custom_login #
# # # # # # # # # # # # # # # # # # # # # # # # # # #
# configuration custom login login request and the response of the callback function, such as: CUSTOM_LOGIN_VIEW = 'ee_official_login. Request. Google. Views. The login'
CUSTOM_LOGIN_VIEW = 'ee_login.enterprise_ldap.views.login'
# configuration custom verify login authentication function, such as: CUSTOM_AUTHENTICATION_BACKEND = 'ee_official_login. Request. Google. Backends. OauthBackend'
CUSTOM_AUTHENTICATION_BACKEND = 'ee_login.enterprise_ldap.backends.ldapbackend'
Copy the code

The configuration file mainly modifies LOGIN_TYPE, CUSTOM_LOGIN_VIEW, and CUSTOM_AUTHENTICATION_BACKEND. Among them: LOGIN_TYPE is the custom login mode. Custom_login is the custom login mode. CUSTOM_LOGIN_VIEW is the function used to process login. Login CUSTOM_AUTHENTICATION_BACKEND in views under enterprise_LDAP is the function for authenticating login, and LDapbackend in backends under enterprise_LDAP

4. Log in to the system

vim enterprise_ldap/views.py
# -*- coding: utf-8 -*-
    
from django.http.response import HttpResponse
from bkaccount.accounts import Account
from django.contrib.sites.shortcuts import get_current_site
from django.template.response import TemplateResponse
from .utils import CustomLoginForm 

def login(request, template_name='login/login.html',
              authentication_form=CustomLoginForm,
              current_app=None, extra_context=None) :
    """ Login processing, """
    account = Account()
    
    REDIRECT_FIELD_NAME = 'c_url'
    redirect_to = request.GET.get(account.REDIRECT_FIELD_NAME, ' ')
    # Get the blue Whale app that the user actually visited
    app_id = request.GET.get('app_id'.' ')
    redirect_field_name = account.REDIRECT_FIELD_NAME
    
    if request.method == 'POST':
        # Implement login authentication through custom form CustomLoginForm
        form = authentication_form(request, data=request.POST)
        if form.is_valid():
            Verify to pass the jump
            return account.login_success_response(request, form, redirect_to, app_id)
    else:
        form = authentication_form(request)
    
    current_site = get_current_site(request)
    context = {
        'form': form,
        redirect_field_name: redirect_to,
        'site': current_site,
        'site_name': current_site.name,
        'app_id': app_id,
    }
    if extra_context is not None:
        context.update(extra_context)
    if current_app is not None:
        request.current_app = current_app
    response = TemplateResponse(request, template_name, context)
    response = account.set_bk_token_invalid(request, response)
    return response
Copy the code

The login function refers to the built-in login function of blue Whale. The difference between them is that different forms are called. Here we call the form after overriding AuthenticationForm (from. utils import CustomLoginForm) : CustomLoginForm, so that login login does not need to go to API, can be implemented locally.

The jump processing after login still uses the original processing.

5. Customize the form

vim enterprise_ldap/utils.py
# -*- coding: utf-8 -*-
from django import forms
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth import authenticate
from common.log import logger

class CustomLoginForm(AuthenticationForm) :
    """ Override the AuthenticationForm class for custom_login """
    def clean(self) :
        username = self.cleaned_data.get('username')
        password = self.cleaned_data.get('password')
        if username and password:
            self.user_cache = authenticate(username=username,
                                           password=password)
            if self.user_cache is None:
                raise forms.ValidationError(
                    self.error_messages['invalid_login'],
                    code='invalid_login',
                    params={'username': self.username_field.verbose_name},
                )
            else:
                super(CustomLoginForm, self).confirm_login_allowed(self.user_cache)

        return self.cleaned_data
Copy the code

We just overwrite the clean method in the parent class AuthenticationForm, because the clean method calls authenticate to verify the user name and password.

6. Authenticate implementation

vim enterprise_ldap/backends.py
# -*- coding: utf-8 -*-

from django.contrib.auth.backends import ModelBackend
from .ldap import SearchLdap
from django.contrib.auth import get_user_model
from bkaccount.constants import RoleCodeEnum
from common.log import logger

class ldapbackend(ModelBackend) :
    def authenticate(self, **credentials) :   
        username = credentials.get('username')
        password = credentials.get('password')
              
        if username and password:
            logger.info("username: %s,password: %s" % (username,password))
            # If the login account is admin, authentication will be performed directly in Blue Whale without LDAP authentication
            if username == 'admin':
                logger.info(U 'username admin, direct blue whale authentication')
                return super(ldapbackend, self).authenticate(username=username, password=password)
            else:
                ldapinfo = SearchLdap()
                resp = ldapinfo.get_user_info(username=username, password=password)
                # if the user exists in LDAP
                if resp["result"] = ="success":
                    Select * from user class Model;
                    user_model = get_user_model()
                    try:
                        user = user_model.objects.get(username=username)
                    except user_model.DoesNotExist:
                        Create User object
                        user = user_model.objects.create_user(username)
                        # get user info, set only when first created, do not update existing information
                        chname = resp['data'] ['chname']
                        phone = resp['data'] ['mobile']
                        email = resp['data'] ['email']
                        user.chname = chname
                        user.phone = phone
                        user.email = email
                        user.save()
                        # Set the role of the new user to administrator
                        logger.info(U 'Create user: %s Permission: %s' % (chname, U 'Ordinary user'))
                        result, message = user_model.objects.modify_user_role(username, RoleCodeEnum.STAFF)
                    return user             
                else:
                    return None
        else:
            return None
Copy the code

Mainly implement authenticate function:

  • 1. After logging in to ldap, filter cn, mail, and mobile fields and check whether they exist in the Blue Whale database. If they do not exist, create a user and grant the administrator role.

    Obtain user information in LDAP through enterprise_ldap/ldap.py.

  • 2. If the login user is admin, blue Whale authentication is performed directly.

7. Ldap obtains user information

vim enterprise_ldap/ldap.py
# -*- coding: utf-8 -*-

from ldap3 import Connection, Server, SUBTREE
from common.log import logger

class SearchLdap:
    host = '10.90.10.123'
    port = 389
    ldap_base = 'ou=People,dc=test,dc=cn'
    def get_user_info(self, **kwargs) :
        
        username = kwargs.get("username")
        password = kwargs.get("password")

        ldap_user = 'cn='+username+', '+self.ldap_base

        try:
            Establish a connection with LDAP
            s = Server(host=self.host, port=self.port, use_ssl=False, get_info='ALL', connect_timeout=5)
            #bind opens the connection
            c = Connection(s, user=ldap_user, password=password, auto_bind='NONE', version=3, authentication='SIMPLE', client_strategy='SYNC', auto_referrals=True, check_names=True, read_only=True, lazy=False, raise_exceptions=False)
    
            c.bind()
            logger.info(c.result)
            # authentication is correct - SUCCESS is incorrect -invalidCredentials
            if c.result['description'] = ='success':
                res = c.search(search_base=self.ldap_base, search_filter = "(cn="+username+")", search_scope = SUBTREE, attributes = ['cn'.'mobile'.'mail'], paged_size = 5)
                if res:
                    attr_dict = c.response[0] ["attributes"]
                    chname = attr_dict['cn'] [0]
                    email = attr_dict['mail'] [0]
                    mobile = attr_dict['mobile'] [0]           
                    data = {
                        'username': "%s" % username,
                        'password': "%s" % password,
                        'chname': "%s" % chname,
                        'email': "%s" % email,
                        'mobile' : "%s" % mobile,
                    }
                    logger.info(U 'LDAP successfully matched user')
                    result = {
                        'result': "success".'message':'Verification successful'.'data':data
                    }
                else:
                    logger.info(U 'LDAP does not have this user information')
                    result = {
                        'result': "null".'message':'result is null'
                    }
                # close the connection
                c.unbind()
            else:
                logger.info(U "User authentication failed")
                result = {
                    'result': "auth_failure".'message': "user auth failure"
                }
                
        except Exception as e:
            logger.info(U 'LDAP connection error: %s' % e)
            result = {
                'result': 'conn_error'.'message': "connect error"
            }
        
        return result
Copy the code

Note:

  • 1. Check whether the LDAP user name and password are successfully logged in to the LDAP server by checking whether the description field of C. ult is SUCCESS. Otherwise, the ldap server can connect to and filter information even if the authentication fails. At this point, blue whale login will occur, as long as the account is in LDAP, even if the password is not correct can be successfully logged in.

  • 2. The ldap login user name must be cn=test,ou=People,dc=test,dc=cn. Otherwise, information can be filtered normally.

8. Restart the login service

/data/install/bkcec stop paas login
/data/install/bkcec start paas login
Copy the code

9. View logs

cd /data/bkce/logs/open_paas/
login_uwsgi.log   login.log
Copy the code

Feedback from netizens:

  1. Since my environment uses open-LDAP, the user name and CN name are the same.

    According to user feedback, authentication fails when AD domain is used. The problem lies in the LDAP connection mode.

    AD authentication requires only the account password, for example, ldap_user=’domain\’+username.

    After the user logged in, an error occurred after the user logged in, and the paAS was restarted.

  2. An empty __init__.py file must be stored in the directory; otherwise, the module cannot be imported, for example, enterprise_ldap.backends.

For other parameters, adjust them based on the actual environment.