美文网首页Go语言Golang程序员
用 Go 写一个轻量级的 ldap 测试工具

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

作者: freedomkk_qfeng | 来源:发表于2018-03-15 17:47 被阅读580次
    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/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

    以上

    转载授权

    CC BY-SA

    相关文章

      网友评论

      本文标题:用 Go 写一个轻量级的 ldap 测试工具

      本文链接:https://www.haomeiwen.com/subject/xmtixftx.html