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

前言

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

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

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

image.png

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

由于認(rèn)證實(shí)際是由 API 來(lái)完成的。因此要實(shí)現(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)證校驗(yàn)。認(rèn)證成功后,先通過(guò) Get User info by name 接口判斷用戶是否存在。若不存在通過(guò) Create User 接口創(chuàng)建用戶。若存在則將用戶名和 token 傳遞給 API,API 給予直接放行。需要小幅修改 API 模塊和 Dashboard 模塊
  3. Dashboard 上進(jìn)行 ldap 認(rèn)證校驗(yàn)。認(rèn)證成功后,先通過(guò) Get User info by name 接口判斷用戶是否存在。若不存在通過(guò) Create User 接口創(chuàng)建用戶。若存在則通過(guò) Change User's Password 接口將他的密碼進(jìn)行本地更新。然后使用用戶+密碼正常調(diào)用 Login 接口認(rèn)證。只需要修改 Dashboard 模塊

ldap 認(rèn)證

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

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,并且無(wú)法支持多個(gè) ou 子樹(shù)。實(shí)際上,ldap 認(rèn)證時(shí),更常見(jiàn)的做法是配置一個(gè) ldap 的管理員賬號(hào)。先由管理員賬號(hào)根據(jù)登錄的用戶名, search 出用戶的 dn,再使用這個(gè) dn 與用戶密碼進(jìn)行 bind 操作,進(jìn)行認(rèn)證校驗(yà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)

一種實(shí)現(xiàn)

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

但是目前的實(shí)現(xiàn)有點(diǎn)不太完整,我們來(lái)看代碼。

以下是 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)證通過(guò)時(shí),dashboard 會(huì)通過(guò) api 創(chuàng)建一個(gè)本地賬號(hào),并將 ldap 用戶認(rèn)證時(shí)的密碼作為本地用戶的密碼。之后再登陸時(shí),實(shí)際上就用的這個(gè)本地密碼來(lái)做本地用戶的認(rèn)證了。

顯然當(dāng)時(shí)作者就發(fā)現(xiàn)了這個(gè)實(shí)現(xiàn)不完整。因?yàn)槿绻脩粼?ldap 上修改了密碼,這個(gè)修改并不會(huì)反饋到 Open-Falcon 中。他依然只能使用老密碼進(jìn)行認(rèn)證

#TODO: update password in db if ldap password changed

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

我們需要用到以下幾個(gè) API

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

API 的調(diào)用,只需要通過(guò)login 接口獲取 Apitoken。請(qǐng)求其他接口時(shí),把 Apitoken 放在請(qǐng)求的 header 里就好了。API 是 REST 風(fēng)格的,非常簡(jiǎn)單易用。我們以獲取 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)

哪里不對(duì)

相信你也覺(jué)得,把 ldap 用戶的密碼本地存一份總感覺(jué)有點(diǎn)怪怪的……

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

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

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

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

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

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

另一種實(shí)現(xiàn)

簡(jiǎn)單來(lái)講,盡量減少對(duì) API 的改動(dòng),同時(shí)要考慮擴(kuò)展性。以后前端再加其他的認(rèn)證,不需要再次改動(dòng) API。

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

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

Admin 用戶們自身的 SSO 怎么處理呢?直接允許與他們平級(jí)的 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 上的邏輯就很簡(jiǎn)單了
/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)

簡(jiǎn)而言之,本地已有賬號(hào),Admin Login 之,本地尚無(wú)賬號(hào),先創(chuàng)建,再 Admin Login

結(jié)束語(yǔ)

本文所有代碼的完整版本均可在以下兩個(gè) 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)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,595評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,560評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,035評(píng)論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,814評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,224評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評(píng)論 3 442
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,444評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,988評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,804評(píng)論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,998評(píng)論 1 370
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評(píng)論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,237評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,665評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,927評(píng)論 1 287
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,706評(píng)論 3 393
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,993評(píng)論 2 374

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