[轉載]用 Go 寫一個輕量級的 ldap 測試工具

freedomkk發表於2018-03-15

轉自用 Go 寫一個輕量級的 ldap 測試工具

image.png

前言

這是一個輪子。

作為一個在高校裡混的 IT,LDAP 我們其實都蠻熟悉的,因為在高校中使用 LDAP 來做統一認證還蠻普遍的。對於 LDAP 的管理員而言,LDAP 的各種操作自然有產品對應的管理工具來處理,但對於需要整合 LDAP 的使用者而言,我們經常需要做一些 LDAP 的測試來作為整合時的對比驗證,腦補以下場景:

系統除錯ing
乙:“LDAP 認證走不通啊,你們的 LDAP 是不是有問題哦”
默默掏出測試工具
甲:“你看,毫無壓力”
乙:“我再查檢視~”

另外,高校間協作共享會比較多一些,例如通過一些聯邦式的認證聯盟來讓聯盟內的成員互相信任身份認證的結果,從而支援一些跨校協作的應用。在國外應用的比較多的是基於 Shibboleth 的聯盟。國內在上海有一個基於相同技術框架的聯盟,稱之為上海市教育認證聯盟。 image.png

我校作為上海聯盟的主要技術支援方,我經常得和各個學校的 LDAP 打交道。遠端支援當然只有 ssh 了。此時要測試 LDAP,LdapBrowser 之類的工具在純 CLI 環境下沒法用,openldap 的 client 又顯得過於麻煩,所以就造個輪子咯。

需求

這個輪子需求大概是這個樣子

  1. 跨平臺,木有依賴,開箱即用。用 Go 來擼一個就能很好的滿足這個需求。
  2. 簡單無腦一點,搞複雜了就沒意思了
  3. 做到 ldap 的認證和查詢就夠了。增刪改涉及 schema 以及不同 LDAP 產品實現時的標準差異,要做到相容通用會比較麻煩。反正這一塊的需求管理員用產品自帶的控制檯就好了嘛,我們的測試工具的就不折騰了
  4. 支援批量查詢和批量認證的測試
  5. 提供個簡單的 HTTP API,必要時也可以提供基於 http 的遠端測試。
  6. 好吧,還可以學習 Golang ~

用 Go 操作 LDAP

我們可以用 https://github.com/go-ldap/ldap 這個庫來操作 LDAP 他的 example 給的非常的詳細,基本看一遍就可以開始抄了。。。

我們拿其中 userAuthentication 的 example 來舉個例子,下為 example 中的示例程式碼,我增加了若干註釋說明

func Example_userAuthentication() {
    // The username and password we want to check
    // 用來認證的使用者名稱和密碼
    username := "someuser"
    password := "userpassword"

    // 用來獲取查詢許可權的 bind 使用者。如果 ldap 禁止了匿名查詢,那我們就需要先用這個帳戶 bind 以下才能開始查詢
    // bind 的賬號通常要使用完整的 DN 資訊。例如 cn=manager,dc=example,dc=org
    // 在 AD 上,則可以用諸如 mananger@example.org 的方式來 bind
    bindusername := "readonly"
    bindpassword := "password"

    l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", "ldap.example.com", 389))
    if err != nil {
        log.Fatal(err)
    }
    defer l.Close()

    // Reconnect with TLS
    // 建立 StartTLS 連線,這是建立純文字上的 TLS 協議,允許你將非加密的通訊升級為 TLS 加密而不需要另外使用一個新的埠。
    // 郵件的 POP3 ,IMAP 也有支援類似的 StartTLS,這些都是有 RFC 的
    err = l.StartTLS(&tls.Config{InsecureSkipVerify: true})
    if err != nil {
        log.Fatal(err)
    }

    // First bind with a read only user
    // 先用我們的 bind 賬號給 bind 上去
    err = l.Bind(bindusername, bindpassword)
    if err != nil {
        log.Fatal(err)
    }

    // Search for the given username
    // 這樣我們就有查詢許可權了,可以構造查詢請求了
    searchRequest := ldap.NewSearchRequest(
        // 這裡是 basedn,我們將從這個節點開始搜尋
        "dc=example,dc=com",
        // 這裡幾個引數分別是 scope, derefAliases, sizeLimit, timeLimit,  typesOnly
        // 詳情可以參考 RFC4511 中的定義,文末有連結
        ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, 
        // 這裡是 LDAP 查詢的 Filter。這個例子例子,我們通過查詢 uid=username 且 objectClass=organizationalPerson。
        // username 即我們需要認證的使用者名稱
        fmt.Sprintf("(&(objectClass=organizationalPerson)(uid=%s))", username),
        // 這裡是查詢返回的屬性,以陣列形式提供。如果為空則會返回所有的屬性
        []string{"dn"},
        nil,
    )
    // 好了現在可以搜尋了,返回的是一個陣列
    sr, err := l.Search(searchRequest)
    if err != nil {
        log.Fatal(err)
    }

    // 如果沒有資料返回或者超過1條資料返回,這對於使用者認證而言都是不允許的。
    // 前這意味著沒有查到使用者,後者意味著存在重複資料
    if len(sr.Entries) != 1 {
        log.Fatal("User does not exist or too many entries returned")
    }

    // 如果沒有意外,那麼我們就可以獲取使用者的實際 DN 了
    userdn := sr.Entries[0].DN

    // Bind as the user to verify their password
    // 拿這個 dn 和他的密碼去做 bind 驗證
    err = l.Bind(userdn, password)
    if err != nil {
        log.Fatal(err)
    }

    // Rebind as the read only user for any further queries
    // 如果後續還需要做其他操作,那麼使用最初的 bind 賬號重新 bind 回來。恢復初始許可權。
    err = l.Bind(bindusername, bindpassword)
    if err != nil {
        log.Fatal(err)
    }
}

總結:

  1. 建立連線
  2. 使用 bind 使用者先 bind 以獲取許可權
  3. 根據使用者名稱對應的屬性寫 searchfilter,結合 basedn 進行查詢
  4. 如果需要認證,用查到的 dn 進行 bind 驗證
  5. 如果還要繼續查詢/認證,rebind 回初始的 bind 使用者上
  6. 關閉連線

命令列

作為一個 cli 工具,命令列部分的設計是很重要的。考慮我們所需要實現的功能

  • 使用者查詢
  • 使用者認證
  • 用特定的 filter 查詢
  • 批量認證
  • 批量查詢

比如可以按這個方式進行羅列 image.png Go 由一個非常好的 cli 庫 cobra,我們就用它來做輪子。

cobra 用起來容易上手,我同樣貼一段他的 example 程式碼來加以註釋來說明

package main

import (
  "fmt"
  "strings"

  "github.com/spf13/cobra"
)

func main() {
  // 給後面的 Flags 用的
  var echoTimes int

  // cobra 以層次的方式組織命令。從 rootCmd 開始,每一個命令都通過一個 struct 來配置命令的相關資訊
  // 這一行本來在 example 的最下面,我給挪上來了
  var rootCmd = &cobra.Command{Use: "app"}

  // 不同於 rootCmd,我們開始給出比較詳細的配置了
  var cmdPrint = &cobra.Command{
  // 命令的名稱,同時 [string to print] 等會在 help 時作為 usage 的內容輸出
    Use:   "print [string to print]",
  // help 時作為 Available Commands 中,cmd 後的短描述
    Short: "Print anything to the screen",
  // help 時作為 cmd 的長描述
    Long: `print is for printing anything back to the screen.
For many years people have printed back to the screen.`,
  // 限制命令最小引數輸入為1,還有其他的引數限制,詳見 github 上的說明
    Args: cobra.MinimumNArgs(1),
  // 命令執行的函式,把命令要乾的事情放在這裡就好了
    Run: func(cmd *cobra.Command, args []string) {
      fmt.Println("Print: " + strings.Join(args, " "))
    },
  }

  var cmdEcho = &cobra.Command{
    Use:   "echo [string to echo]",
    Short: "Echo anything to the screen",
    Long: `echo is for echoing anything back.
Echo works a lot like print, except it has a child command.`,
    Args: cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
      fmt.Println("Print: " + strings.Join(args, " "))
    },
  }

  var cmdTimes = &cobra.Command{
    Use:   "times [# times] [string to echo]",
    Short: "Echo anything to the screen more times",
    Long: `echo things multiple times back to the user by providing
a count and a string.`,
    Args: cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
      for i := 0; i < echoTimes; i++ {
        fmt.Println("Echo: " + strings.Join(args, " "))
      }
    },
  }

  // 這裡為 cmdTimes 對應命令設定了一個 Flag 引數
  // 型別為 Int,輸入方式為 `--times` 或者 `-t`,預設值時 1,繫結到最開始宣告的 `echoTimes` 上。
  cmdTimes.Flags().IntVarP(&echoTimes, "times", "t", 1, "times to echo the input")

  // rootCmd 後面 Add 了 cmdPrint, cmdEcho
  // 也就是說初始的兩個命令是 `print` 和 `echo`
  rootCmd.AddCommand(cmdPrint, cmdEcho)
  // cmdEcho 後面 Add 了 cmdTimes
  // 所以 `echo` 後面還有一個命令時 `times`
  cmdEcho.AddCommand(cmdTimes)
  rootCmd.Execute()
}

實際生產環境中,我們可以每個命令的相關程式碼單獨放在一個 .go 檔案中,這樣看起來會比較清晰一些。像這樣

├── cmd
│   ├── auth.go
│   ├── http.go
│   ├── root.go
│   ├── search.go
│   ├── utils.go
│   └── version.go
├── main.go

API

API 可以用著名的 beego 框架來搞。 beego 的文件 非常詳細,就不再贅述了。

基於 beego ,我們提供以下 API,把命令列支援的功能都搬過來。

GET /api/v1/ldap/health
ldap 健康狀態監測。請求的時候就去嘗試連線一下 ldap,用 bind 賬號 bind 測試下。成功的話就返回 ok,否則給個錯。 
GET /api/v1/ldap/search/filter/:filter
根據 ldap filter 來做查詢
GET /api/v1/ldap/search/user/:username
根據使用者名稱來查詢
POST /api/v1/ldap/search/multi
根據使用者名稱同時查詢多個使用者,以 application/json 方式傳送請求資料,例:
["user1","user2","user3"]
POST /api/v1/ldap/auth/single
單個使用者的認證測試,以 application/json 方式傳送請求資料,例:
{
    "username": "user",
    "password": "123456"
}
POST /api/v1/ldap/auth/multi
單個使用者的認證測試,以 application/json 方式傳送請求資料,例:
[{
    "username": "user1",
    "password": "123456"
}, {
    "username": "user2",
    "password": "654321"
}]

輪子

那麼這個輪子已經造好了。ldao-test-tool

程式碼結構

# tree
.
├── cfg.json.example
├── cmd
│   ├── auth.go
│   ├── http.go
│   ├── root.go
│   ├── search.go
│   ├── utils.go
│   └── version.go
├── g
│   ├── cfg.go
│   └── const.go
├── http
│   ├── controllers
│   │   ├── authMulti.go
│   │   ├── authSingle.go
│   │   ├── default.go
│   │   ├── health.go
│   │   ├── searchFilter.go
│   │   ├── searchMulti.go
│   │   └── searchUser.go
│   ├── http.go
│   └── router.go
├── LICENSE
├── main.go
├── models
│   ├── funcs.go
│   ├── ldap.go
│   └── ldap_test.go
└── README.MD

編譯

go get ./...
go build

release

可以直接下載編譯好的 release 版本

提供 win64 和 linux64 兩個平臺的可執行檔案

https://github.com/shanghai-edu/ldap-test-tool/releases/

配置檔案

預設配置檔案為目錄下的 cfg.json,也可以使用 -c--config 來載入自定義的配置檔案。

openldap 配置示例

{
    "ldap": {
        "addr": "ldap.example.org:389",
        "baseDn": "dc=example,dc=org",
        "bindDn": "cn=manager,dc=example,dc=org",
        "bindPass": "password",
        "authFilter": "(&(uid=%s))",
        "attributes": ["uid", "cn", "mail"],
        "tls":        false,
        "startTLS":   false
    },
    "http": {
        "listen": "0.0.0.0:8888"
    }
}

AD 配置示例

{
    "ldap": {
        "addr": "ad.example.org:389",
        "baseDn": "dc=example,dc=org",
        "bindDn": "manager@example.org",
        "bindPass": "password",
        "authFilter": "(&(sAMAccountName=%s))",
        "attributes": ["sAMAccountName", "displayName", "mail"],
        "tls":        false,
        "startTLS":   false
    },
    "http": {
        "listen": "0.0.0.0:8888"
    }
}

命令體系

命令列部分使用 cobra 框架,可以使用 help 命令檢視命令的使用方式

# ./ldap-test-tool help
ldap-test-tool is a simple tool for ldap test
build by shanghai-edu.
Complete documentation is available at github.com/shanghai-edu/ldap-test-tool

Usage:
  ldap-test-tool [flags]
  ldap-test-tool [command]

Available Commands:
  auth        Auth Test
  help        Help about any command
  http        Enable a http server for ldap-test-tool
  search      Search Test
  version     Print the version number of ldap-test-tool

Flags:
  -c, --config string   load config file. default cfg.json (default "cfg.json")
  -h, --help            help for ldap-test-tool

Use "ldap-test-tool [command] --help" for more information about a command.

認證

./ldap-test-tool auth -h
Auth Test

Usage:
  ldap-test-tool auth [flags]
  ldap-test-tool auth [command]

Available Commands:
  multi       Multi Auth Test
  single      Single Auth Test

Flags:
  -h, --help   help for auth

Global Flags:
  -c, --config string   load config file. default cfg.json (default "cfg.json")

Use "ldap-test-tool auth [command] --help" for more information about a command.
單使用者測試

命令列說明

Single Auth Test

Usage:
  ldap-test-tool auth single [username] [password] [flags]

Flags:
  -h, --help   help for single

Global Flags:
  -c, --config string   load config file. default cfg.json (default "cfg.json")

示例

./ldap-test-tool auth single qfeng 123456
LDAP Auth Start 
==================================

qfeng auth test successed 

==================================
LDAP Auth Finished, Time Usage 47.821884ms 
批量測試

命令列說明

# ./ldap-test-tool auth multi -h
Multi Auth Test

Usage:
  ldap-test-tool auth multi [filename] [flags]

Flags:
  -h, --help   help for multi

Global Flags:
  -c, --config string   load config file. default cfg.json (default "cfg.json")

示例

# cat authusers.txt 
qfeng,123456
qfengtest,111111

使用者名稱和密碼以逗號分隔(csv風格) authusers.txt 中有兩個使用者,密碼正確的 qfeng 和密碼錯誤的 qfengtest

# ./ldap-test-tool auth multi authusers.txt 
LDAP Multi Auth Start 
==================================

Successed count 1 
Failed count 1 
Failed users:
 -- User: qfengtest , Msg: Cannot find such user 

==================================
LDAP Multi Auth Finished, Time Usage 49.582994ms 

查詢

# ./ldap-test-tool search -h
Search Test

Usage:
  ldap-test-tool search [flags]
  ldap-test-tool search [command]

Available Commands:
  filter      Search By Filter
  multi       Search Multi Users
  user        Search Single User

Flags:
  -h, --help   help for search

Global Flags:
  -c, --config string   load config file. default cfg.json (default "cfg.json")

Use "ldap-test-tool search [command] --help" for more information about a command.
[root@wiki-qfeng ldap-test-tool]# 
單使用者查詢

命令列說明

# ./ldap-test-tool search user -h
Search Single User

Usage:
  ldap-test-tool search user [username] [flags]

Flags:
  -h, --help   help for user

Global Flags:
  -c, --config string   load config file. default cfg.json (default "cfg.json")
[root@wiki-qfeng ldap-test-tool]# 

示例

# ./ldap-test-tool search user qfeng
LDAP Search Start 
==================================

DN: uid=qfeng,ou=people,dc=example,dc=org
Attributes:
 -- uid  : qfeng 
 -- cn   : 馮騏測試 
 -- mail : qfeng@example.org

==================================
LDAP Search Finished, Time Usage 44.711268ms 

PS: 如果屬性有多值,將以 ; 分割

LDAP Filter 查詢
# ./ldap-test-tool search filter -h
Search By Filter

Usage:
  ldap-test-tool search filter [searchFilter] [flags]

Flags:
  -h, --help   help for filter

Global Flags:
  -c, --config string   load config file. default cfg.json (default "cfg.json")

示例

# ./ldap-test-tool search filter "(cn=*測試)"
LDAP Search By Filter Start 
==================================

DN: uid=test1,ou=people,dc=example,dc=org
Attributes:
 -- uid  : test1 
 -- cn   : 一號測試 
 -- mail : test1@example.org 

DN: uid=test2,ou=people,dc=example,dc=org
Attributes:
 -- uid  : test2 
 -- cn   : 二號測試 
 -- mail : test2@example.org 

DN: uid=test3,ou=people,dc=example,dc=org
Attributes:
 -- uid  : test3
 -- cn   : 三號測試 
 -- mail : test3@example.org 

results count  3

==================================
LDAP Search By Filter Finished, Time Usage 46.071833ms 
批量查詢測試

命令列說明

# ./ldap-test-tool search multi -h
Search Multi Users

Usage:
  ldap-test-tool search multi [filename] [flags]

Flags:
  -f, --file   output search to users.csv, failed search to failed.csv
  -h, --help   help for multi

Global Flags:
  -c, --config string   load config file. default cfg.json (default "cfg.json")

示例

# cat searchusers.txt 
qfeng
qfengtest
nofounduser

searchuser.txt 中有三個使用者,其中 nofounduser 是不存在的使用者

# ldap-test-tool.exe search multi .\searchusers.txt
LDAP Multi Search Start
==================================

Successed users:

DN: uid=qfeng,ou=people,dc=example,dc=org
Attributes:
 -- uid  : qfeng
 -- cn   : 馮騏
 -- mail : qfeng@example.org

DN: uid=qfengtest,ou=people,dc=example,dc=org
Attributes:
 -- uid  : qfengtest
 -- cn   : 馮騏測試
 -- mail : qfeng@example.org

nofounduser : Cannot find such user

Successed count 2
Failed count 1

==================================
LDAP Multi Search Finished, Time Usage 134.744ms

當使用 -f 選項時,查詢的結果將輸出到 csv 中。csv 將以配置檔案中 attributes 的屬性作為 title。因此當使用 -f 選項時,attributes 不得為空。

# ./ldap-test-tool search multi searchusers.txt -f
LDAP Multi Search Start 
==================================

OutPut to csv successed

==================================
LDAP Multi Search Finished, Time Usage 88.756956ms 

# ls | grep csv
failed.csv
users.csv

HTTP API

HTTP API 部分使用 beego 框架 使用如下命令開啟 HTTP API

# ldap-test-tool.exe http
2018/03/12 14:30:25 [I] http server Running on http://0.0.0.0:8888
健康狀態

檢測 ldap 健康狀態

# curl http://127.0.0.1:8888/api/v1/ldap/health   
{
  "msg": "ok",
  "success": true
}
查詢使用者

查詢單個使用者資訊

# curl  http://127.0.0.1:8888/api/v1/l ... qfeng
{
  "user": {
    "dn": "uid=qfeng,ou=people,dc=example,dc=org",
    "attributes": {
      "cn": [
        "馮騏"
      ],
      "mail": [
        "qfeng@example.org"
      ],
      "uid": [
        "qfeng"
      ]
    }
  },
  "success": true
}
Filter 查詢

根據 LDAP Filter 查詢

# curl  http://127.0.0.1:8888/api/v1/ldap/search/filter/\(cn=*測試\)
{
  "results": [
    {
      "dn": "uid=test1,ou=people,dc=example,dc=org",
      "attributes": {
        "cn": [
          "一號測試"
        ],
        "mail": [
          "test1@example.org"
        ],
        "uid": [
          "test1"
        ]
      }
    },
    {
      "dn": "uid=test2,ou=people,dc=example,dc=org",
      "attributes": {
        "cn": [
          "二號測試"
        ],
        "mail": [
          "test2@example.org"
        ],
        "uid": [
          "test2"
        ]
      }
    },
    {
      "dn": "uid=test3,ou=people,dc=example,dc=org",
      "attributes": {
        "cn": [
          "三號測試"
        ],
        "mail": [
          "test3@example.org"
        ],
        "uid": [
          "test3"
        ]
      }
    },
  ],
  "success": true
}
多使用者查詢

同時查詢多個使用者,以 application/json 方式傳送請求資料,請求資料示例

["qfeng","qfengtest","nofounduser"]

curl 示例

# curl -X POST  -H 'Content-Type:application/json' -d '["qfeng","qfengtest","nofounduser"]' http://127.0.0.1:8888/api/v1/ldap/search/multi
{
  "success": true,
  "result": {
    "successed": 2,
    "failed": 1,
    "users": [
      {
        "dn": "uid=qfeng,ou=people,dc=example,dc=org",
        "attributes": {
          "cn": [
            "馮騏"
          ],
          "mail": [
            "qfeng@example.org"
          ],
          "uid": [
            "qfeng"
          ]
        }
      },
      {
        "dn": "uid=qfengtest,ou=people,dc=example,dc=org",
        "attributes": {
          "cn": [
            "馮騏測試"
          ],
          "mail": [
            "qfeng@example.org"
          ],
          "uid": [
            "qfengtest"
          ]
        }
      }
    ],
    "failed_messages": [
      {
        "username": "nofounduser",
        "message": "Cannot find such user"
      }
    ]
  }
}

認證

單使用者認證

單個使用者認證測試,以 application/json 方式傳送請求資料,請求資料示例

{
    "username": "qfeng",
    "password": "123456"
}

curl 示例

# curl -X POST  -H 'Content-Type:application/json' -d '{"username":"qfeng","password":"123456"}' http://127.0.0.1:8888/api/v1/ldap/auth/single
{
  "msg": "user 20150073 Auth Successed",
  "success": true
}
多使用者認證

同時發起多個使用者認證測試,以 application/json 方式傳送請求資料,請求資料示例

[{
    "username": "qfeng",
    "password": "123456"
}, {
    "username": "qfengtest",
    "password": "1111111"
}]

curl 示例

# curl -X POST  -H 'Content-Type:application/json' -d '[{"username":"qfeng","password":"123456"},{"username":"qfengtest","password":"1111111"}]' http://127.0.0.1:8888/api/v1/ldap/auth/multi
{
  "success": true,
  "result": {
    "successed": 1,
    "failed": 1,
    "failed_messages": [
      {
        "username": "qfengtest",
        "message": "LDAP Result Code 49 \"Invalid Credentials\": "
      }
    ]
  }
}

參考文件

LDAP WiKi

SSL vs TLS vs STARTTLS

IBM Security Identity Manager V6.0.0.10 - enRoleLDAPConnection.properties

RFC4511

cobra

beego

以上

轉載授權

 CC BY-SA 

相關文章