前言
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
接口
我們可以在 FALCON+ API 上看到所有 API 文檔說明。
由于認(rèn)證實(shí)際是由 API 來完成的脊另。因此要實(shí)現(xiàn) LDAP 認(rèn)證导狡,辦法可能有以下三種
- Dashboard 傳遞用戶名和密碼給 API,增加字段標(biāo)注為 ldap 認(rèn)證用戶偎痛。LDAP 認(rèn)證邏輯由 API 完成烘豌。若用戶不存在,API 視
signup_disable
決定是否創(chuàng)建用戶看彼。需要較大幅度的修改 API 模塊。 - Dashboard 上進(jìn)行 ldap 認(rèn)證校驗(yàn)囚聚。認(rèn)證成功后靖榕,先通過
Get User info by name
接口判斷用戶是否存在。若不存在通過Create User
接口創(chuàng)建用戶顽铸。若存在則將用戶名和 token 傳遞給 API茁计,API 給予直接放行。需要小幅修改 API 模塊和 Dashboard 模塊谓松。 - 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