Skip to content

Commit 05f81f7

Browse files
clementblaiseClément Blaisestefanmcshanejohnweldoncpuschma
authored
Add Entry Unmarshal (#304)
Co-authored-by: Clément Blaise <[email protected]> Co-authored-by: Stefan McShane <[email protected]> Co-authored-by: John Weldon <[email protected]> Co-authored-by: Christopher Puschmann <[email protected]>
1 parent a3dcdda commit 05f81f7

8 files changed

+418
-0
lines changed

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ go 1.14
55
require (
66
github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e
77
github.com/go-asn1-ber/asn1-ber v1.5.4
8+
github.com/stretchr/testify v1.7.2 // indirect
89
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect
910
)

go.sum

+11
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,16 @@ github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzU
22
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
33
github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e h1:ZU22z/2YRFLyf/P4ZwUYSdNCWsMEI0VeyrFoI2rAhJQ=
44
github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
5+
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
6+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
57
github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
68
github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
9+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
10+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
11+
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
12+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
13+
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
14+
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
715
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o=
816
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
917
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -13,3 +21,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
1321
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
1422
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
1523
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
24+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
25+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
26+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

search.go

+100
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package ldap
33
import (
44
"errors"
55
"fmt"
6+
"reflect"
67
"sort"
78
"strings"
89

@@ -161,6 +162,105 @@ func (e *Entry) PrettyPrint(indent int) {
161162
}
162163
}
163164

165+
// Describe the tag to use for struct field tags
166+
const decoderTagName = "ldap"
167+
168+
// readTag will read the reflect.StructField value for
169+
// the key defined in decoderTagName. If omitempty is
170+
// specified, the field may not be filled.
171+
func readTag(f reflect.StructField) (string, bool) {
172+
val, ok := f.Tag.Lookup(decoderTagName)
173+
if !ok {
174+
return f.Name, false
175+
}
176+
opts := strings.Split(val, ",")
177+
omit := false
178+
if len(opts) == 2 {
179+
omit = opts[1] == "omitempty"
180+
}
181+
return opts[0], omit
182+
}
183+
184+
// Unmarshal parses the Entry in the value pointed to by i
185+
//
186+
// Currently, this methods only supports struct fields of type
187+
// string or []string. Other field types will not be regarded.
188+
// If the field type is a string but multiple attribute values
189+
// are returned, the first value will be used to fill the field.
190+
//
191+
// Example:
192+
// type UserEntry struct {
193+
// // Fields with the tag key `dn` are automatically filled with the
194+
// // objects distinguishedName. This can be used multiple times.
195+
// DN string `ldap:"dn"`
196+
//
197+
// // This field will be filled with the attribute value for
198+
// // userPrincipalName. An attribute can be read into a struct field
199+
// // multiple times. Missing attributes will not result in an error.
200+
// UserPrincipalName string `ldap:"userPrincipalName"`
201+
//
202+
// // memberOf may have multiple values. If you don't
203+
// // know the amount of attribute values at runtime, use a string array.
204+
// MemberOf []string `ldap:"memberOf"`
205+
//
206+
// // This won't work, as the field is not of type string. For this
207+
// // to work, you'll have to temporarily store the result in string
208+
// // (or string array) and convert it to the desired type afterwards.
209+
// UserAccountControl uint32 `ldap:"userPrincipalName"`
210+
// }
211+
// user := UserEntry{}
212+
// if err := result.Unmarshal(&user); err != nil {
213+
// // ...
214+
// }
215+
func (e *Entry) Unmarshal(i interface{}) (err error) {
216+
// Make sure it's a ptr
217+
if vo := reflect.ValueOf(i).Kind(); vo != reflect.Ptr {
218+
return fmt.Errorf("ldap: cannot use %s, expected pointer to a struct", vo)
219+
}
220+
221+
sv, st := reflect.ValueOf(i).Elem(), reflect.TypeOf(i).Elem()
222+
// Make sure it's pointing to a struct
223+
if sv.Kind() != reflect.Struct {
224+
return fmt.Errorf("ldap: expected pointer to a struct, got %s", sv.Kind())
225+
}
226+
227+
for n := 0; n < st.NumField(); n++ {
228+
// Holds struct field value and type
229+
fv, ft := sv.Field(n), st.Field(n)
230+
231+
// skip unexported fields
232+
if ft.PkgPath != "" {
233+
continue
234+
}
235+
236+
// omitempty can be safely discarded, as it's not needed when unmarshalling
237+
fieldTag, _ := readTag(ft)
238+
239+
// Fill the field with the distinguishedName if the tag key is `dn`
240+
if fieldTag == "dn" {
241+
fv.SetString(e.DN)
242+
continue
243+
}
244+
245+
values := e.GetAttributeValues(fieldTag)
246+
if len(values) == 0 {
247+
continue
248+
}
249+
250+
switch fv.Interface().(type) {
251+
case []string:
252+
for _, item := range values {
253+
fv.Set(reflect.Append(fv, reflect.ValueOf(item)))
254+
}
255+
case string:
256+
fv.SetString(values[0])
257+
default:
258+
return fmt.Errorf("ldap: expected field to be of type string or []string, got %v", ft.Type)
259+
}
260+
}
261+
return
262+
}
263+
164264
// NewEntryAttribute returns a new EntryAttribute with the desired key-value pair
165265
func NewEntryAttribute(name string, values []string) *EntryAttribute {
166266
var bytes [][]byte

search_test.go

+97
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package ldap
22

33
import (
4+
"github.com/stretchr/testify/assert"
45
"reflect"
56
"testing"
67
)
@@ -48,3 +49,99 @@ func TestGetAttributeValue(t *testing.T) {
4849
t.Errorf("failed to get attribute in changed case")
4950
}
5051
}
52+
53+
func TestEntry_Unmarshal(t *testing.T) {
54+
55+
t.Run("passing a struct should fail", func(t *testing.T) {
56+
entry := &Entry{}
57+
58+
type toStruct struct{}
59+
60+
result := toStruct{}
61+
err := entry.Unmarshal(result)
62+
63+
assert.NotNil(t, err)
64+
})
65+
66+
t.Run("passing a ptr to string should fail", func(t *testing.T) {
67+
entry := &Entry{}
68+
69+
str := "foo"
70+
err := entry.Unmarshal(&str)
71+
72+
assert.NotNil(t, err)
73+
})
74+
75+
t.Run("user struct be decoded", func(t *testing.T) {
76+
entry := &Entry{
77+
DN: "cn=mario,ou=Users,dc=go-ldap,dc=github,dc=com",
78+
Attributes: []*EntryAttribute{
79+
{
80+
Name: "cn",
81+
Values: []string{"mario"},
82+
ByteValues: nil,
83+
},
84+
{
85+
Name: "mail",
86+
Values: []string{"[email protected]"},
87+
ByteValues: nil,
88+
},
89+
},
90+
}
91+
92+
type User struct {
93+
Dn string `ldap:"dn"`
94+
Cn string `ldap:"cn"`
95+
Mail string `ldap:"mail"`
96+
}
97+
98+
expect := &User{
99+
Dn: "cn=mario,ou=Users,dc=go-ldap,dc=github,dc=com",
100+
Cn: "mario",
101+
102+
}
103+
result := &User{}
104+
err := entry.Unmarshal(result)
105+
106+
assert.Nil(t, err)
107+
assert.Equal(t, expect, result)
108+
109+
})
110+
111+
t.Run("group struct be decoded", func(t *testing.T) {
112+
entry := &Entry{
113+
DN: "cn=DREAM_TEAM,ou=Groups,dc=go-ldap,dc=github,dc=com",
114+
Attributes: []*EntryAttribute{
115+
{
116+
Name: "cn",
117+
Values: []string{"DREAM_TEAM"},
118+
ByteValues: nil,
119+
},
120+
{
121+
Name: "member",
122+
Values: []string{"mario", "luigi", "browser"},
123+
ByteValues: nil,
124+
},
125+
},
126+
}
127+
128+
type Group struct {
129+
DN string `ldap:"dn" yaml:"dn" json:"dn"`
130+
CN string `ldap:"cn" yaml:"cn" json:"cn"`
131+
Members []string `ldap:"member"`
132+
}
133+
134+
expect := &Group{
135+
DN: "cn=DREAM_TEAM,ou=Groups,dc=go-ldap,dc=github,dc=com",
136+
CN: "DREAM_TEAM",
137+
Members: []string{"mario", "luigi", "browser"},
138+
}
139+
140+
result := &Group{}
141+
err := entry.Unmarshal(result)
142+
143+
assert.Nil(t, err)
144+
assert.Equal(t, expect, result)
145+
})
146+
147+
}

v3/go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ go 1.14
55
require (
66
github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e
77
github.com/go-asn1-ber/asn1-ber v1.5.4
8+
github.com/stretchr/testify v1.7.2 // indirect
89
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect
910
)

v3/go.sum

+11
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,16 @@ github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzU
22
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
33
github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e h1:ZU22z/2YRFLyf/P4ZwUYSdNCWsMEI0VeyrFoI2rAhJQ=
44
github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
5+
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
6+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
57
github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
68
github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
9+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
10+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
11+
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
12+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
13+
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
14+
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
715
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o=
816
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
917
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -13,3 +21,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
1321
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
1422
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
1523
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
24+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
25+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
26+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

v3/search.go

+100
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package ldap
33
import (
44
"errors"
55
"fmt"
6+
"reflect"
67
"sort"
78
"strings"
89

@@ -161,6 +162,105 @@ func (e *Entry) PrettyPrint(indent int) {
161162
}
162163
}
163164

165+
// Describe the tag to use for struct field tags
166+
const decoderTagName = "ldap"
167+
168+
// readTag will read the reflect.StructField value for
169+
// the key defined in decoderTagName. If omitempty is
170+
// specified, the field may not be filled.
171+
func readTag(f reflect.StructField) (string, bool) {
172+
val, ok := f.Tag.Lookup(decoderTagName)
173+
if !ok {
174+
return f.Name, false
175+
}
176+
opts := strings.Split(val, ",")
177+
omit := false
178+
if len(opts) == 2 {
179+
omit = opts[1] == "omitempty"
180+
}
181+
return opts[0], omit
182+
}
183+
184+
// Unmarshal parses the Entry in the value pointed to by i
185+
//
186+
// Currently, this methods only supports struct fields of type
187+
// string or []string. Other field types will not be regarded.
188+
// If the field type is a string but multiple attribute values
189+
// are returned, the first value will be used to fill the field.
190+
//
191+
// Example:
192+
// type UserEntry struct {
193+
// // Fields with the tag key `dn` are automatically filled with the
194+
// // objects distinguishedName. This can be used multiple times.
195+
// DN string `ldap:"dn"`
196+
//
197+
// // This field will be filled with the attribute value for
198+
// // userPrincipalName. An attribute can be read into a struct field
199+
// // multiple times. Missing attributes will not result in an error.
200+
// UserPrincipalName string `ldap:"userPrincipalName"`
201+
//
202+
// // memberOf may have multiple values. If you don't
203+
// // know the amount of attribute values at runtime, use a string array.
204+
// MemberOf []string `ldap:"memberOf"`
205+
//
206+
// // This won't work, as the field is not of type string. For this
207+
// // to work, you'll have to temporarily store the result in string
208+
// // (or string array) and convert it to the desired type afterwards.
209+
// UserAccountControl uint32 `ldap:"userPrincipalName"`
210+
// }
211+
// user := UserEntry{}
212+
// if err := result.Unmarshal(&user); err != nil {
213+
// // ...
214+
// }
215+
func (e *Entry) Unmarshal(i interface{}) (err error) {
216+
// Make sure it's a ptr
217+
if vo := reflect.ValueOf(i).Kind(); vo != reflect.Ptr {
218+
return fmt.Errorf("ldap: cannot use %s, expected pointer to a struct", vo)
219+
}
220+
221+
sv, st := reflect.ValueOf(i).Elem(), reflect.TypeOf(i).Elem()
222+
// Make sure it's pointing to a struct
223+
if sv.Kind() != reflect.Struct {
224+
return fmt.Errorf("ldap: expected pointer to a struct, got %s", sv.Kind())
225+
}
226+
227+
for n := 0; n < st.NumField(); n++ {
228+
// Holds struct field value and type
229+
fv, ft := sv.Field(n), st.Field(n)
230+
231+
// skip unexported fields
232+
if ft.PkgPath != "" {
233+
continue
234+
}
235+
236+
// omitempty can be safely discarded, as it's not needed when unmarshalling
237+
fieldTag, _ := readTag(ft)
238+
239+
// Fill the field with the distinguishedName if the tag key is `dn`
240+
if fieldTag == "dn" {
241+
fv.SetString(e.DN)
242+
continue
243+
}
244+
245+
values := e.GetAttributeValues(fieldTag)
246+
if len(values) == 0 {
247+
continue
248+
}
249+
250+
switch fv.Interface().(type) {
251+
case []string:
252+
for _, item := range values {
253+
fv.Set(reflect.Append(fv, reflect.ValueOf(item)))
254+
}
255+
case string:
256+
fv.SetString(values[0])
257+
default:
258+
return fmt.Errorf("ldap: expected field to be of type string or []string, got %v", ft.Type)
259+
}
260+
}
261+
return
262+
}
263+
164264
// NewEntryAttribute returns a new EntryAttribute with the desired key-value pair
165265
func NewEntryAttribute(name string, values []string) *EntryAttribute {
166266
var bytes [][]byte

0 commit comments

Comments
 (0)