写点什么

用 Go 写一个轻量级的 ldap 测试工具

用户头像
冯骐
关注
发布于: 2021 年 03 月 04 日
用 Go 写一个轻量级的 ldap 测试工具

前言


这是一个轮子。


作为一个在高校里混的 IT,LDAP 我们其实都蛮熟悉的,因为在高校中使用 LDAP 来做统一认证还蛮普遍的。对于 LDAP 的管理员而言,LDAP 的各种操作自然有产品对应的管理工具来处理,但对于需要集成 LDAP 的用户而言,我们经常需要做一些 LDAP 的测试来作为集成时的对比验证,脑补以下场景:


系统调试ing乙:“LDAP 认证走不通啊,你们的 LDAP 是不是有问题哦”默默掏出测试工具甲:“你看,毫无压力”乙:“我再查查看~”
复制代码

另外,高校间协作共享会比较多一些,例如通过一些联邦式的认证联盟来让联盟内的成员互相信任身份认证的结果,从而支持一些跨校协作的应用。在国外应用的比较多的是基于 Shibboleth 的联盟。国内在上海有一个基于相同技术框架的联盟,称之为上海市教育认证联盟。



我校作为上海联盟的主要技术支持方,我经常得和各个学校的 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 查询

  • 批量认证

  • 批量查询



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 providinga 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/healthldap 健康状态监测。请求的时候就去尝试连接一下 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 helpldap-test-tool is a simple tool for ldap testbuild 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 -hAuth 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 123456LDAP Auth Start ==================================
qfeng auth test successed
==================================LDAP Auth Finished, Time Usage 47.821884ms
复制代码
批量测试


命令行说明


# ./ldap-test-tool auth multi -hMulti 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,123456qfengtest,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 -hSearch 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 -hSearch 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 qfengLDAP Search Start ==================================

DN: uid=qfeng,ou=people,dc=example,dc=orgAttributes: -- uid : qfeng -- cn : 冯骐测试 -- mail : qfeng@example.org

==================================LDAP Search Finished, Time Usage 44.711268ms
复制代码

PS: 如果属性有多值,将以 ; 分割


LDAP Filter 查询


# ./ldap-test-tool search filter -hSearch 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=orgAttributes: -- uid : test1 -- cn : 一号测试 -- mail : test1@example.org

DN: uid=test2,ou=people,dc=example,dc=orgAttributes: -- uid : test2 -- cn : 二号测试 -- mail : test2@example.org

DN: uid=test3,ou=people,dc=example,dc=orgAttributes: -- uid : test3 -- cn : 三号测试 -- mail : test3@example.org
results count 3
==================================LDAP Search By Filter Finished, Time Usage 46.071833ms
复制代码
批量查询测试


命令行说明


# ./ldap-test-tool search multi -hSearch 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 qfengqfengtestnofounduser
复制代码

searchuser.txt 中有三个用户,其中 nofounduser 是不存在的用户


# ldap-test-tool.exe search multi .\searchusers.txtLDAP Multi Search Start==================================
Successed users:
DN: uid=qfeng,ou=people,dc=example,dc=orgAttributes: -- uid : qfeng -- cn : 冯骐 -- mail : qfeng@example.org

DN: uid=qfengtest,ou=people,dc=example,dc=orgAttributes: -- uid : qfengtest -- cn : 冯骐测试 -- mail : qfeng@example.org
nofounduser : Cannot find such user
Successed count 2Failed count 1
==================================LDAP Multi Search Finished, Time Usage 134.744ms
复制代码

当使用 -f 选项时,查询的结果将输出到 csv 中。csv 将以配置文件中 attributes 的属性作为 title。因此当使用 -f 选项时,attributes 不得为空。


# ./ldap-test-tool search multi searchusers.txt -fLDAP Multi Search Start ==================================
OutPut to csv successed
==================================LDAP Multi Search Finished, Time Usage 88.756956ms
# ls | grep csvfailed.csvusers.csv
复制代码

HTTP API


HTTP API 部分使用 beego 框架

使用如下命令开启 HTTP API


# ldap-test-tool.exe http2018/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/ldap/search/user/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


以上



原文于 2018 年 3 月首发于简书,搬家存档。

行文有微调。


发布于: 2021 年 03 月 04 日阅读数: 21
用户头像

冯骐

关注

教育行业码农 2020.06.19 加入

一个教育行业的码农

评论

发布
暂无评论
用 Go 写一个轻量级的 ldap 测试工具