Open-Falcon 中的 LDAP 認(rèn)證

前言

Open-Falcon 是當(dāng)下國內(nèi)最流行的開源監(jiān)控框架之一。LDAP 是一種輕量級的目錄協(xié)議,廣泛應(yīng)用于統(tǒng)一身份認(rèn)證中。自然的,我們的監(jiān)控系統(tǒng)也需要對接 LDAP 進(jìn)行認(rèn)證。因此我們來研究一下 Open-Falcon 中如何通過 LDAP 來進(jìn)行身份認(rèn)證。

認(rèn)證結(jié)構(gòu)

由于在 Open-Falcon 2.0 以后已經(jīng)實現(xiàn)了前后端的分離。Dashboard 本身并不承擔(dān)用戶的認(rèn)證和鑒權(quán)等工作,他只是把用戶發(fā)送給 API 模塊,由 API 進(jìn)行認(rèn)證并賦予權(quán)限。例如這個 login 接口

image.png

我們可以在 FALCON+ API 上看到所有 API 文檔說明。

由于認(rèn)證實際是由 API 來完成的。因此要實現(xiàn) LDAP 認(rèn)證,辦法可能有以下三種

  1. Dashboard 傳遞用戶名和密碼給 API,增加字段標(biāo)注為 ldap 認(rèn)證用戶。LDAP 認(rèn)證邏輯由 API 完成。若用戶不存在,API 視 signup_disable 決定是否創(chuàng)建用戶。需要較大幅度的修改 API 模塊
  2. Dashboard 上進(jìn)行 ldap 認(rèn)證校驗。認(rèn)證成功后,先通過 Get User info by name 接口判斷用戶是否存在。若不存在通過 Create User 接口創(chuàng)建用戶。若存在則將用戶名和 token 傳遞給 API,API 給予直接放行。需要小幅修改 API 模塊和 Dashboard 模塊
  3. Dashboard 上進(jìn)行 ldap 認(rèn)證校驗。認(rèn)證成功后,先通過 Get User info by name 接口判斷用戶是否存在。若不存在通過 Create User 接口創(chuàng)建用戶。若存在則通過 Change User's Password 接口將他的密碼進(jìn)行本地更新。然后使用用戶+密碼正常調(diào)用 Login 接口認(rèn)證。只需要修改 Dashboard 模塊

ldap 認(rèn)證

目前 dashboard 中的 ldap 認(rèn)證,是基于配置文件模板來綁定用戶的方式來做的。即 LDAP_BINDDN_FMT 這個配置

LDAP_SERVER = os.environ.get("LDAP_SERVER","ldap.forumsys.com:389")
LDAP_BASE_DN = os.environ.get("LDAP_BASE_DN","dc=example,dc=com")
LDAP_BINDDN_FMT = os.environ.get("LDAP_BINDDN_FMT","uid=%s,dc=example,dc=com")
LDAP_SEARCH_FMT = os.environ.get("LDAP_SEARCH_FMT","uid=%s")

這需要用戶知道自己在 ldap 中的完整 dn,并且無法支持多個 ou 子樹。實際上,ldap 認(rèn)證時,更常見的做法是配置一個 ldap 的管理員賬號。先由管理員賬號根據(jù)登錄的用戶名, search 出用戶的 dn,再使用這個 dn 與用戶密碼進(jìn)行 bind 操作,進(jìn)行認(rèn)證校驗。類似這樣

        cli.bind_s(bind_dn, bind_pass, ldap.AUTH_SIMPLE)
        result = cli.search_s(base_dn, ldap.SCOPE_SUBTREE, search_filter, config.LDAP_ATTRS)
        log.debug("ldap result: %s" % result)
        user_dn = result[0][0]
        cli.bind_s(user_dn, password, ldap.AUTH_SIMPLE)

一種實現(xiàn)

從 Dashboard 的代碼里可以看到,事實上當(dāng)下 Dashboard 中選擇的是第三種實現(xiàn)方式。也就是 ldap 認(rèn)證通過后,同步到本地。再通過標(biāo)準(zhǔn) Login 接口進(jìn)行認(rèn)證。這樣可以不必修改 API 模塊,改動會比較小。

但是目前的實現(xiàn)有點不太完整,我們來看代碼。

以下是 dashboard 中 rrd/view/auth/auth.py 的代碼片段

        if ldap == "1":
            try:
                ldap_info = view_utils.ldap_login_user(name, password)

                h = {"Content-type":"application/json"}
                d = {
                    "name": name,
                    "password": password,
                    "cnname": ldap_info['cnname'],
                    "email": ldap_info['email'],
                    "phone": ldap_info['phone'],
                }

                r = requests.post("%s/user/create" %(config.API_ADDR,), \
                        data=json.dumps(d), headers=h)
                log.debug("%s:%s" %(r.status_code, r.text))

                #TODO: update password in db if ldap password changed
            except Exception as e:
                ret["msg"] = str(e)
                return json.dumps(ret)

可以看到,當(dāng) ldap 認(rèn)證通過時,dashboard 會通過 api 創(chuàng)建一個本地賬號,并將 ldap 用戶認(rèn)證時的密碼作為本地用戶的密碼。之后再登陸時,實際上就用的這個本地密碼來做本地用戶的認(rèn)證了。

顯然當(dāng)時作者就發(fā)現(xiàn)了這個實現(xiàn)不完整。因為如果用戶在 ldap 上修改了密碼,這個修改并不會反饋到 Open-Falcon 中。他依然只能使用老密碼進(jìn)行認(rèn)證

#TODO: update password in db if ldap password changed

所以第一種辦法就是把這個實現(xiàn)給補(bǔ)完。讓用戶每次認(rèn)證的時候都更新一下本地的密碼。

我們需要用到以下幾個 API

  • Login —— 用于獲取 token
  • Get User info by name —— 用于確認(rèn)用戶是否存在
  • Change User's Password —— 用于更新用戶的密碼
  • Create User —— 用于創(chuàng)建用戶

API 的調(diào)用,只需要通過login 接口獲取 Apitoken。請求其他接口時,把 Apitoken 放在請求的 header 里就好了。API 是 REST 風(fēng)格的,非常簡單易用。我們以獲取 Apitoken 和 獲取用戶 id 為例,代碼如下:

def get_Apitoken(name, password):
     d = {"name": name, "password": password}
     h = {"Content-type":"application/json"}
     r = requests.post("%s/user/login" %(config.API_ADDR,), \
             data=json.dumps(d), headers=h)
     if r.status_code != 200:
         raise Exception("%s %s" %(r.status_code, r.text)) 
     sig = json.loads(r.text)["sig"]
     return json.dumps({"name":name,"sig":sig})
 
 def get_user_id(name, Apitoken):
     h = {"Content-type":"application/json","Apitoken":Apitoken}    
     r = requests.get("%s/user/name/%s" %(config.API_ADDR,name), headers=h)
     if r.status_code != 200:
         user_id = -1
         return user_id
     user_id = json.loads(r.text)["id"]
     return user_id

現(xiàn)在可以補(bǔ)完認(rèn)證的邏輯了。

LDAP 認(rèn)證 ——》 認(rèn)證成功 ——》 判斷用戶是否存在(Get User info by name ) ——》 不存在 ——》 創(chuàng)建用戶(Create User) ——》 本地認(rèn)證(Login)

LDAP 認(rèn)證 ——》 認(rèn)證成功 ——》 判斷用戶是否存在(Get User info by name ) ——》 存在 ——》 更新本地密碼(Change User's Password)——》 本地認(rèn)證(Login)

代碼片段如下

        if ldap == "1":
            try:
                ldap_info = view_utils.ldap_login_user(name, password)

                user_info = {
                    "name": name,
                    "password": password,
                    "cnname": ldap_info['cnname'],
                    "email": ldap_info['email'],
                    "phone": ldap_info['phone'],
                }

                Apitoken = view_utils.get_Apitoken(config.API_USER, config.API_PASS)

                user_id = view_utils.get_user_id(name, Apitoken)
                
                if user_id > 0:
                    view_utils.update_password(user_id, password, Apitoken)
                    # if user exist, update password
                else:
                    view_utils.create_user(user_info)
                    # create user , signup must be enabled
                    
            except Exception as e:
                ret["msg"] = str(e)
                return json.dumps(ret)

哪里不對

相信你也覺得,把 ldap 用戶的密碼本地存一份總感覺有點怪怪的……

況且,這樣的邏輯意味著 ldap 用戶實際上可以使用這個密碼進(jìn)行本地認(rèn)證,即便不勾選 ldap 選項。雖然說這意味著 ldap 宕機(jī)的時候能繼續(xù)保持登陸可用性,但是同時也意味著如果用戶修改了 ldap 的密碼,或者修改了ldap 中的狀態(tài)(比如禁用),但是再他下一次登陸 dashboard 之前,Open-Falcon 本地的密碼并不會隨之更新。

我們假設(shè)某個用戶被盜了,管理員緊急的鎖掉了他的 LDAP 賬號。但是 Open-Falcon 并不能感知到!盜號者依然可以用這個用戶的密碼在 dashboard 上完成認(rèn)證。這其實存在安全隱患。

所以似乎修改 API 模塊已經(jīng)不可避免了。那是把 ldap 的認(rèn)證邏輯直接做進(jìn) API 模塊,還是 API 模塊加一個接口來信任 ldap 認(rèn)證的結(jié)果呢?

讓我們考慮的稍微遠(yuǎn)一點點。

ldap 認(rèn)證實際上可以視作是一種第三方認(rèn)證。從擴(kuò)展性上來講,我們將來可能還要進(jìn)一步集成其他方式的第三方認(rèn)證,比如 CAS,Oauth2,OpenID 等。

這些邏輯如果都直接做進(jìn) API 的話,未免顯得太羅嗦。況且有些不太符合前后端分離的設(shè)計初衷。

另一種實現(xiàn)

簡單來講,盡量減少對 API 的改動,同時要考慮擴(kuò)展性。以后前端再加其他的認(rèn)證,不需要再次改動 API。

所以就給 API 加個接口來信任第三方認(rèn)證吧,盡可能簡單一點,復(fù)用 API 現(xiàn)有的授權(quán)邏輯。基于角色的 Apitoken 進(jìn)行權(quán)限控制。例如這樣:

一個擁有 Admin 權(quán)限(Role = 1)的用戶,通過該賬號申請的 Apitoken ,可以調(diào)用Admin Login 接口,認(rèn)證普通角色( Role = 0 )的用戶。

Admin 用戶們自身的 SSO 怎么處理呢?直接允許與他們平級的 Admin 用戶擁有 Admin Login 權(quán)限似乎不太合適。所以我們限制只有 root( Role = 2 ) 才能夠 Admin Login Admin

falcon-plus/modules/api/app/controller/uic/session_controller.go 修改后的代碼片段

func AdminLogin(c *gin.Context) {
    inputs := APIAdminLoginInput{}
    if err := c.Bind(&inputs); err != nil {
        h.JSONR(c, badstatus, "name is blank")
        return
    }
    name := inputs.Name

    user := uic.User{
        Name: name,
    }
    adminuser, err := h.GetUser(c)
    if err != nil {
        h.JSONR(c, badstatus, err.Error())
        return
    }

    db.Uic.Where(&user).Find(&user)
    switch {
    case user.ID == 0:
        h.JSONR(c, badstatus, "no such user")
        return
    case user.Role >= adminuser.Role:
        h.JSONR(c, badstatus, "API_USER not admin, no permissions can do this")
        return
    }
    var session uic.Session
    s := db.Uic.Table("session").Where("uid = ?", user.ID).Scan(&session)
    if s.Error != nil && s.Error.Error() != "record not found" {
        h.JSONR(c, badstatus, s.Error)
        return
    } else if session.ID == 0 {
        session.Sig = utils.GenerateUUID()
        session.Expired = int(time.Now().Unix()) + 3600*24*30
        session.Uid = user.ID
        db.Uic.Create(&session)
    }
    log.Debugf("session: %v", session)
    resp := struct {
        Sig   string `json:"sig,omitempty"`
        Name  string `json:"name,omitempty"`
        Admin bool   `json:"admin"`
    }{session.Sig, user.Name, user.IsAdmin()}
    h.JSONR(c, resp)
    return
}

現(xiàn)在 Dashboard 上的邏輯就很簡單了
/dashboard/rrd/view/auth/auth.py 修改后的代碼片段

        if ldap == "1":
            try:
                ldap_info = view_utils.ldap_login_user(name, password)
                password = id_generator()
                user_info = {
                    "name": name,
                    "password": password,
                    "cnname": ldap_info['cnname'],
                    "email": ldap_info['email'],
                    "phone": ldap_info['phone'],
                }
                Apitoken = view_utils.get_Apitoken(config.API_USER, config.API_PASS)

                ut = view_utils.admin_login_user(name, Apitoken)
                if not ut:
                    view_utils.create_user(user_info)
                    ut = view_utils.admin_login_user(name, Apitoken)
                    #if user not exist, create user , signup must be enabled
                ret["data"] = {
                        "name": ut.name,
                        "sig": ut.sig,
                }
                return json.dumps(ret)

簡而言之,本地已有賬號,Admin Login 之,本地尚無賬號,先創(chuàng)建,再 Admin Login

結(jié)束語

本文所有代碼的完整版本均可在以下兩個 PR 找到
https://github.com/open-falcon/dashboard/pull/76
https://github.com/open-falcon/falcon-plus/pull/305

以上

轉(zhuǎn)載授權(quán)

CC BY-SA

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容