Go 中的 LDAP 入门

2024-01-25 11:08:00
CJL
转贴:
cybernetist
292


原文章:https://cybernetist.com/2020/05/18/getting-started-with-go-ldap/


最近,我必须为我的一个客户编写大量与 Active Directory (AD) 交互的 Go 代码。AD 使用轻量级目录访问协议 (LDAP) [1] 进行客户端-服务器通信。LDAP 是一个非常成熟且强大的与目录服务交互的协议,尽管我的一些朋友认为它现在已经成为过去的遗物了。我不同意这种观点,但我的解释可能需要另一篇博客文章。

大约两三年前,我面临着类似的 LDAP 挑战。我必须编写一些 Go 代码,它将使用 LDAP 来实现组成员资格和其他一些与授权相关的事情。当时 LDAP Go 库的状况非常糟糕。我最终shell使用了众所周知的LDAP 命令行工具不幸的是,这一次不是一个选择,但幸运的是我很快了解到 Go LDAP 中的内容已经发生了变化并且变得更好!

这篇博文提供了这个精彩模块的基本介绍go-ldap当我开始为我的客户开发 LDAP 项目时,我希望有这样的介绍。此外,我想要一些参考指南,以便我将来可以在需要时查阅。我希望您不仅会发现这篇文章有帮助,而且还能学到新东西。让我们开始吧!

连接

在对 AD 执行任何操作之前,您需要连接到 AD 服务器。下面我们将AD服务器称为LDAP服务器;对我来说,它感觉更自然,语义更正确。另外,我在这篇文章中描述的 Go 模块称为ldap-go,所以让我们继续使用 LDAP。go-ldap模块提供了多个选项供您连接到 LDAP 服务器。让我们更详细地了解其中的一些。

LDAP 连接的所有变体均由该函数处理DialURL模块中还有一些其他功能可用,但文档表明它们已被弃用,以支持DialURL功能。顾名思义,您提供一个 URL,该函数会尝试连接到远程 LDAP 服务器,如果成功则返回连接句柄。

请参阅下面的示例代码:

ldapURL := "ldaps://ldap.example.com:636" l, err := ldap.DialURL(ldapURL) if err != nil {  log.Fatal(err) } defer l.Close() 

上面的代码尝试与远程服务器建立TLS连接。DialURL从 URL 方案推断连接类型,在本例中设置为ldaps(注意末尾的“s”)。

如果您需要更细粒度的 TLS 配置,该函数可以通过附加参数接受自定义 TLS 配置:

ldapURL := "ldaps://ldap.example.com:636" l, err := ldap.DialURL(ldapURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: true})) if err != nil {  log.Fatal(err) } defer l.Close() 

注意: 上面的示例代码仅供说明之用!创建 TLS 连接时切勿跳过 TLS 验证!

如果您不想使用 TLS,您可以在 URL 方案中省略“s”,如下所示:

ldapURL := "ldap://ldap.example.com:389" l, err := ldap.DialURL(ldapURL) if err != nil {  log.Fatal(err) } defer l.Close() 

您还可以省略 LDAP URL 中的端口。为了简洁起见,上面的代码示例显示了它。如果省略端口号,该DialURL函数会自动使用特定 URL 方案的默认端口号,即 636 用于ldaps://“明文”(明文)连接,389 用于“明文”lpap://连接。默认 LDAP 端口号也可以通过全局变量DefaultLdapsPort和访问DefaultLdapPort

或者,您可以使用该NewConn(conn net.Conn, isTLS bool)函数,该函数允许您传入您可能通过不同方式建立的原始连接net.Conn(请参阅此处)。

最后,您还可以使用以下函数将现有的“明文”连接升级到 TLS 连接StartTLS()

l, err := DialURL("ldap://ldap.example.com:389") if err != nil {  log.Fatal(err) } defer l.Close()  // Now reconnect with TLS err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) if err != nil {  log.Fatal(err) } 

现在您已经了解了如何连接到 LDAP 服务器,我们可以继续下一步:Binding。

绑定

绑定是 LDAP 服务器对客户端进行身份验证的步骤。如果客户端成功通过身份验证,服务器将根据其权限授予其访问权限。

使用 进行 LDAP 绑定有多种不同的方法ldap-go让我们从最简单的情况开始:未经身份验证的绑定。

有时,LDAP 服务器允许未经身份验证的客户端进行有限的只读访问。未经身份验证的 LDAP 绑定 [1]、[2] 可以按如下方式完成

// connect code as shown earlier  err = l.UnauthenticatedBind("cn=read-only-admin,dc=example,dc=com") if err != nil {  log.Fatal(err) } 

但是,如果您需要进行身份验证,有两个选项可供您选择:SimpleBindBind后者是前者的一个很好的包装,所以我更喜欢在我的代码中使用它:

// connect code as shown earlier  err = l.Bind("cn=read-only-admin,dc=example,dc=com", "p4ssw0rd") if err != nil {  log.Fatal(err) } 

最后,您还可以执行“外部”绑定,根据官方 RFC [3],它 允许客户端请求服务器使用通过机制外部方式建立的凭据来对客户端进行身份验证

这实际上意味着客户端绑定到 UNIX 套接字(您的 URL 模式必须是ldapi://),并且 SASL/TLS 身份验证通过 UNIX 套接字“间接”发生。

我从未使用过这种形式的身份验证,所以我不能过多谈论它,但我猜测它在“sidecar”场景中可能很有用,在这种情况下,您通过 UNIX 套接字与 sidecar 进程进行通信,其中 sidecar 进程在其中进行通信代表您处理 LDAP 身份验证(和通信)。

LDAP 增删改查

现在我们已经连接并通过了身份验证,我们可以造成一些损害。如果用于身份验证的帐户具有适当的权限,您可以启动AddMod验证、Search设置和Del设置 LDAP 记录。让我们更详细地了解其中的每一个。

一般来说,您将对三个基本记录执行操作:组、用户和计算机[帐户]。

添加和修改

您可以使用该Add功能创建新的 LDAP 记录。它接受一个参数:一个AddRequest您可以手动制作AddRequestAddRequest结构体与其所有字段一起导出),也可以使用库为您提供的简单辅助函数。我们将在下面看看这两种情况。

我决定将添加和修改示例组合在一起,因为它们比我最初想象的更相关,稍后您将看到!

添加组

将组添加到 AD 花了我一些时间才弄清楚,但在阅读了各种 AD 文档页面后,我最终得到了这样的结果:

// connect code comes here  addReq := ldap.NewAddRequest("CN=testgroup,ou=Groups,dc=example,dc=com", []ldap.Control{}) var attrs []ldap.Attribute attr := ldap.Attribute{  Type: "objectClass",  Vals: []string{"top", "group"}, } attrs = append(attrs, attr)  attr = ldap.Attribute{  Type: "name",  Vals: []string{"testgroup"}, } attrs = append(attrs, attr)  attr = ldap.Attribute{  Type: "sAMAccountName",  Vals: []string{"testgroup"}, } attrs = append(attrs, attr)  // make the group writable i.e. modifiable // https://docs.microsoft.com/en-us/windows/win32/adschema/a-instancetype instanceType := 0x00000004 attr = ldap.Attribute{  Type: "instanceType",  Vals: []string{fmt.Sprintf("%d", instanceType}, } attrs = append(attrs, attr)  // make the group domain local and the group to be a security group // https://docs.microsoft.com/en-us/windows/win32/adschema/a-grouptype groupType := 0x00000004 | 0x80000000 attr = ldap.Attribute{  Type: "groupType",  Vals: []string{fmt.Sprintf("%d", groupType)}, } attrs = append(attrs, attr)  addReq.Attributes = attrs  if err := l.AddRequest(addReq); err != nil {  log.Fatal("error adding group:", addReq, err) } 

现在,这段代码看起来有点冗长,确实如此。有一种更简洁的方法可以做同样的事情,但为了简洁起见,我想展示上面的代码,因为这就是我的第一个代码的样子。

这是一个更好的方法,可以完成同样的事情:

// connect code comes here  addReq := ldp.NewAddRequest("CN=testgroup,ou=Groups,dc=example,dc=com", []ldp.Control{})  addReq.Attribute("objectClass", []string{"top", "group"}) addReq.Attribute("name", []string{"testgroup"}) addReq.Attribute("sAMAccountName", []string{"testgroup"}) addReq.Attribute("instanceType", []string{fmt.Sprintf("%d", 0x00000004}) addReq.Attribute("groupType", []string{fmt.Sprintf("%d", 0x00000004 | 0x80000000)})  if err := l.AddRequest(addReq); err != nil {  log.Fatal("error adding group:", addReq, err) } 

有几件事需要强调。首先,您需要确保您的objectClass属性属于正确的类型("top""group")。

接下来的instanceType十六进制数字看起来有点吓人,但如果您想创建“可写”即可修改的组记录,这正是 AD 所期望的[4]。

最后,这个groupType属性看起来更疯狂!事实证明,如果您希望您的组具有域本地范围,同时它也是一个安全组(而不是通讯组),您需要对 AD 文档 [5] 中定义的标志进行按位操作。

完成此操作后,您就可以开始了。您可以使用熟悉的 LDAP 命令行工具验证是否已创建组:

ldapsearch -LLL -o ldif-wrap=no -b "OU=testgroup,OU=Group,dc=example,dc=com" \  -D "${LDAP_USERNAME_DN}" -w "${LDAP_BIND_PASSWD}" -h "${LDAP_HOST}" \  '(CN=testgroup)' cn 

添加用户

添加用户变得更加复杂,这让我很困惑,因为对我来说如何做到这一点并不是很明显。最好的学习方法就是这样做,让我们看一个具体的例子。

假设您要创建一个新的 LDAP 用户并为其分配某个密码。另外假设您不希望密码过期。我最初认为可以解决这个问题的方法是以AddRequest与前面的组示例中所示类似的方式简单地完成所有这些操作。

我想我会找到正确的 LDAP 属性,将它们放入 中AddRequest,这样就完成了工作。我错了,我花了一些时间才弄清楚!

事实证明,关键是将整个过程分为三个步骤:

  1. 创建禁用帐户
  2. 设置用户密码
  3. 启用用户帐户

知道了这一点,步骤 1 的生成代码就非常简单:

// connect code comes here  addReq = ldp.NewAddRequest("CN=fooUser,OU=Users,dc=example,dc=com", []ldp.Control{}) addReq.Attribute("objectClass", []string{"top", "organizationalPerson", "user", "person"}) addReq.Attribute("name", []string{"fooUser"}) addReq.Attribute("sAMAccountName", []string{"fooUser"}) addReq.Attribute("userAccountControl", []string{fmt.Sprintf("%d", 0x0202}) addReq.Attribute("instanceType", []string{fmt.Sprintf("%d", 0x00000004}) addReq.Attribute("userPrincipalName", []string{"fooUser@example.com"}) addReq.Attribute("accountExpires", []string{fmt.Sprintf("%d", 0x00000000})  addReq.Attributes = attrs  if err := l.AddRequest(addReq); err != nil {  log.Fatal("error adding service:", addReq, err) } 

现在帐户已创建,我们可以继续第二步:设置用户密码。

Active Directory 服务器以小尾数法存储带引号的密码UTF16,编码为base64这有点啰嗦,但这确实是我在文档中找到的。幸运的是,对我来说,Linux 提供了一些方便的实用程序来为您处理所有这些事情。要以正确的格式创建新密码,您可以运行如下所示的命令:

echo -n "\"password\"" | iconv -f UTF8 -t UTF16LE | base64 -w 0 

现在您已经为新用户生成了密码,是时候在 LDAP 服务器中设置它了。您可以通过修改用户帐户的属性来完成此操作unicodePwd下面的代码展示了如何实现这一点:

// connect code comes here  // https://github.com/golang/text // According to the MS docs the password needs to be enclosed in quotes o_O utf16 := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM) pwdEncoded, err := utf16.NewEncoder().String(fmt.Sprintf("%q", userPasswd)) if err != nil {  log.Fatal(err) }  modReq := ldap.NewModifyRequest("CN=fooUser,OU=Users,dc=example,dc=com", []ldap.Control{}) modReq.Replace("unicodePwd", []string{pwdEncoded})  if err := l.ModRequest(modReq); err != nil {  log.Fatal("error setting user password:", modReq, err) } 

注意:处理unicode代码实际上来自Gotext [6]

最后,您需要通过修改其属性来启用用户帐户[再次]:

modReq := ldap.NewModifyRequest("CN=fooUser,OU=Users,dc=example,dc=com", []ldap.Control{}) modReq.Replace("userAccountControl", []string{fmt.Sprintf("%d", 0x0200})  if err := l.ModRequest(modReq); err != nil {  log.Fatal("error enabling user account:", modReq, err) } 

同样,您可以轻松验证用户是否已创建:

$ ldapsearch -LLL -o ldif-wrap=no -b "OU=fooUser,OU=Users,dc=example,dc=com" \  -D "{LDAP_USERNAME_DN}" -w "${LDAP_BIND_PASSWD}" -h "${LDAP_HOST}" \  '(CN=fooUser)' cn 

添加机器帐户

您还可以在 LDAP 中创建计算机(也称为服务)帐户,这些帐户通常与 Kerberos [7] 结合使用,用于存储服务属性并授予对不同服务和资源的访问权限。

计算机帐户的创建方式与用户帐户的创建方式类似,但也有一些区别。

必须"computer"向属性值列表添加值objectClass

发表评论
评论通过审核后显示。
流量统计