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

发表于:2018-3-29 11:00

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:冯琪    来源:小专栏

  前言
  这是一个轮子。
  作为一个在高校里混的 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 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"
}
}

上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。
21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号