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)實(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 文檔說明。

由于認(rèn)證實(shí)際是由 API 來完成的脊另。因此要實(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)證成功后靖榕,先通過 Get User info by name 接口判斷用戶是否存在。若不存在通過 Create User 接口創(chuàng)建用戶顽铸。若存在則將用戶名和 token 傳遞給 API茁计,API 給予直接放行。需要小幅修改 API 模塊和 Dashboard 模塊谓松。
  3. Dashboard 上進(jìn)行 ldap 認(rèn)證校驗(yà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 這個(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演怎,并且無法支持多個(gè) ou 子樹。實(shí)際上避乏,ldap 認(rèn)證時(shí)爷耀,更常見的做法是配置一個(gè) ldap 的管理員賬號。先由管理員賬號根據(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)證通過后锄贼,同步到本地票灰。再通過標(biāo)準(zhǔn) Login 接口進(jìn)行認(rèn)證。這樣可以不必修改 API 模塊,改動(dòng)會(huì)比較小屑迂。

但是目前的實(shí)現(xiàn)有點(diǎ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)證通過時(shí)庸汗,dashboard 會(huì)通過 api 創(chuàng)建一個(gè)本地賬號,并將 ldap 用戶認(rèn)證時(shí)的密碼作為本地用戶的密碼手报。之后再登陸時(shí)蚯舱,實(shí)際上就用的這個(gè)本地密碼來做本地用戶的認(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)用,只需要通過login 接口獲取 Apitoken阳藻。請求其他接口時(shí)晰奖,把 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 用戶的密碼本地存一份總感覺有點(diǎn)怪怪的……

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

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

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

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

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

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

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

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

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

一個(gè)擁有 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é)束語

本文所有代碼的完整版本均可在以下兩個(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)容合作請聯(lián)系作者
  • 序言:七十年代末莺奸,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子冀宴,更是在濱河造成了極大的恐慌灭贷,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,222評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件略贮,死亡現(xiàn)場離奇詭異甚疟,居然都是意外死亡仗岖,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,455評論 3 385
  • 文/潘曉璐 我一進(jìn)店門古拴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來箩帚,“玉大人,你說我怎么就攤上這事黄痪〗襞粒” “怎么了?”我有些...
    開封第一講書人閱讀 157,720評論 0 348
  • 文/不壞的土叔 我叫張陵桅打,是天一觀的道長是嗜。 經(jīng)常有香客問我,道長挺尾,這世上最難降的妖魔是什么鹅搪? 我笑而不...
    開封第一講書人閱讀 56,568評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮遭铺,結(jié)果婚禮上丽柿,老公的妹妹穿的比我還像新娘。我一直安慰自己魂挂,他們只是感情好甫题,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,696評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著涂召,像睡著了一般坠非。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上果正,一...
    開封第一講書人閱讀 49,879評論 1 290
  • 那天炎码,我揣著相機(jī)與錄音,去河邊找鬼秋泳。 笑死潦闲,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的迫皱。 我是一名探鬼主播矫钓,決...
    沈念sama閱讀 39,028評論 3 409
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼舍杜!你這毒婦竟也來了新娜?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,773評論 0 268
  • 序言:老撾萬榮一對情侶失蹤既绩,失蹤者是張志新(化名)和其女友劉穎概龄,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體饲握,經(jīng)...
    沈念sama閱讀 44,220評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡私杜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,550評論 2 327
  • 正文 我和宋清朗相戀三年蚕键,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片衰粹。...
    茶點(diǎn)故事閱讀 38,697評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡锣光,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出铝耻,到底是詐尸還是另有隱情誊爹,我是刑警寧澤,帶...
    沈念sama閱讀 34,360評論 4 332
  • 正文 年R本政府宣布瓢捉,位于F島的核電站频丘,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏泡态。R本人自食惡果不足惜搂漠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,002評論 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望某弦。 院中可真熱鬧桐汤,春花似錦、人聲如沸靶壮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,782評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽亮钦。三九已至,卻和暖如春充活,著一層夾襖步出監(jiān)牢的瞬間蜂莉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,010評論 1 266
  • 我被黑心中介騙來泰國打工混卵, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留映穗,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,433評論 2 360
  • 正文 我出身青樓幕随,卻偏偏與公主長得像蚁滋,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子赘淮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,587評論 2 350

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