Skip to content

Commit cb46c96

Browse files
committed
feature(ldap): Adds objectSid and objectUUID formatting when using Active Directory
#### Overview Adds support for formatting `objectSid` and `objectGUID` for use with the ldap connector and Active Directory. #### What this PR does / why we need it Closes: #3128 Active Directory stores these two attributes as byte arrays. This change converts them to strings, allowing them to be used for any `*Attr` but intended for `idAttr`. #### Special notes for your reviewer
1 parent ab64ed7 commit cb46c96

3 files changed

Lines changed: 188 additions & 1 deletion

File tree

connector/ldap/ldap.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"strings"
1515

1616
"github.com/go-ldap/ldap/v3"
17+
"github.com/google/uuid"
1718

1819
"github.com/dexidp/dex/connector"
1920
)
@@ -243,6 +244,49 @@ func parseScope(s string) (int, bool) {
243244
return 0, false
244245
}
245246

247+
// formatSidAttr converts the objectSid byte array returned from
248+
// Active Directory into a readable string format like S-1-5-21...
249+
func formatSidAttr(b []byte) (string, error) {
250+
251+
minbytes := 1 + 1 + 6
252+
maxSubAuthorities := 15
253+
maxBytes := 1 + 1 + 6 + 4*maxSubAuthorities
254+
255+
if (len(b) < minbytes) || (len(b) > maxBytes) {
256+
return "", fmt.Errorf("Out of Range, length for SID expected to between: %d and %d bytes", minbytes, maxBytes)
257+
}
258+
259+
revision := int(b[0])
260+
if revision != 1 {
261+
return "", fmt.Errorf("SIDs with revision other than '1' are not supported.")
262+
}
263+
264+
subAuthoritiesLength := int(b[1])
265+
if subAuthoritiesLength > maxSubAuthorities {
266+
return "", fmt.Errorf("The number of sub-authorities must not exceed %d", maxSubAuthorities)
267+
}
268+
269+
totalLength := 1 + 1 + 6 + 4*subAuthoritiesLength
270+
if len(b) < totalLength {
271+
return "", fmt.Errorf("Out of Range: Length of bytes mismatch")
272+
}
273+
274+
var iav int
275+
for i, x := range b[2:8] {
276+
iav = iav | int(x)<<(8*(5-i))
277+
}
278+
s := fmt.Sprintf("S-%d-%d", revision, iav)
279+
280+
for i := range subAuthoritiesLength {
281+
var sub int
282+
for i, x := range b[8+4*i : 12+4*i] {
283+
sub = sub | int(x)<<(8*i)
284+
}
285+
s += fmt.Sprintf("-%d", sub)
286+
}
287+
return s, nil
288+
}
289+
246290
// Build a list of group attr name to user attr value matchers.
247291
// Function exists here to allow backward compatibility between old and new
248292
// group to user matching implementations.
@@ -493,6 +537,24 @@ func (c *ldapConnector) getAttrs(e ldap.Entry, name string) []string {
493537

494538
func (c *ldapConnector) getAttr(e ldap.Entry, name string) string {
495539
if a := c.getAttrs(e, name); len(a) > 0 {
540+
541+
if name == "objectSid" {
542+
sid, err := formatSidAttr([]byte(a[0]))
543+
if err != nil {
544+
c.logger.Error("ldap: attribute failed to be formatted as objectSid", "attribute", a[0])
545+
return ""
546+
}
547+
return sid
548+
}
549+
550+
if name == "objectGUID" {
551+
uuid_string, err := uuid.FromBytes([]byte(a[0]))
552+
if err != nil {
553+
c.logger.Error("ldap: attribute failed to be formatted as objectGUID", "attribute", a[0])
554+
return ""
555+
}
556+
return uuid_string.String()
557+
}
496558
return a[0]
497559
}
498560
return ""

connector/ldap/ldap_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,85 @@ func TestNestedGroups(t *testing.T) {
642642
runTests(t, connectLDAP, c, tests)
643643
}
644644

645+
func TestFormatSid(t *testing.T) {
646+
tests := []struct {
647+
name string
648+
bytes []byte
649+
want string
650+
wantErr bool
651+
}{
652+
{name: "Contoso\\Jane", bytes: []byte{1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 220, 244, 220, 59, 131, 61, 43, 70, 130, 139, 166, 40, 210, 4, 0, 0}, want: "S-1-5-21-1004336348-1177238915-682003330-1234"},
653+
{name: "null sid", bytes: []byte{1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, want: "S-1-0-0"},
654+
{name: "world", bytes: []byte{1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0}, want: "S-1-1-0"},
655+
{name: "empty string", bytes: nil, wantErr: true},
656+
{name: "invalid sid", bytes: []byte{}, wantErr: true},
657+
{name: "invalid revision", bytes: []byte{2, 1, 0, 0, 0, 0, 0, 0}, wantErr: true},
658+
{name: "too many sub auth", bytes: []byte{1, 100, 0, 0, 0, 0, 0, 0}, wantErr: true},
659+
}
660+
661+
for _, tt := range tests {
662+
t.Run(tt.name, func(t *testing.T) {
663+
sid, err := formatSidAttr(tt.bytes)
664+
if (err != nil) != tt.wantErr {
665+
t.Fatalf("formatSidAttr() error = %v, wantErr %v", err, tt.wantErr)
666+
}
667+
if !tt.wantErr {
668+
if actual := sid; actual != tt.want {
669+
t.Errorf("expected %v, got %v", tt.want, actual)
670+
}
671+
}
672+
})
673+
}
674+
}
675+
676+
func TestObjectSID(t *testing.T) {
677+
c := &Config{}
678+
c.UserSearch.BaseDN = "ou=People,ou=TestObjectSid,dc=example,dc=org"
679+
c.UserSearch.NameAttr = "cn"
680+
c.UserSearch.EmailAttr = "mail"
681+
c.UserSearch.IDAttr = "objectSid"
682+
c.UserSearch.Username = UsernameAttributes{"cn"}
683+
684+
tests := []subtest{
685+
{
686+
name: "validpassword",
687+
username: "jane",
688+
password: "foo",
689+
want: connector.Identity{
690+
UserID: "S-1-5-21-1004336348-1177238915-682003330-1234",
691+
Username: "jane",
692+
Email: "janedoe@example.com",
693+
EmailVerified: true,
694+
},
695+
},
696+
}
697+
runTests(t, connectLDAP, c, tests)
698+
}
699+
700+
func TestObjectGUID(t *testing.T) {
701+
c := &Config{}
702+
c.UserSearch.BaseDN = "ou=People,ou=TestAdObjects,dc=example,dc=org"
703+
c.UserSearch.NameAttr = "cn"
704+
c.UserSearch.EmailAttr = "mail"
705+
c.UserSearch.IDAttr = "objectGUID"
706+
c.UserSearch.Username = UsernameAttributes{"cn"}
707+
708+
tests := []subtest{
709+
{
710+
name: "validpassword",
711+
username: "jane",
712+
password: "foo",
713+
want: connector.Identity{
714+
UserID: "123e4567-e89b-12d3-a456-426614174000",
715+
Username: "jane",
716+
Email: "janedoe@example.com",
717+
EmailVerified: true,
718+
},
719+
},
720+
}
721+
runTests(t, connectLDAP, c, tests)
722+
}
723+
645724
func getenv(key, defaultVal string) string {
646725
if val := os.Getenv(key); val != "" {
647726
return val

connector/ldap/testdata/schema.ldif

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,4 +526,50 @@ objectClass: inetOrgPerson
526526
sn: doe
527527
cn: jane
528528
mail: janedoe@example.com
529-
userpassword: foo
529+
userpassword: foo
530+
531+
########################################################################
532+
533+
# Mock AD Attrs: objectGUID & objectSid
534+
535+
dn: cn=mock_ad_attributes,cn=schema,cn=config
536+
objectClass: olcSchemaConfig
537+
cn: mock_ad_attributes
538+
olcAttributeTypes: ( 1.2.840.113556.1.4.2
539+
NAME 'objectGUID'
540+
DESC 'Microsoft Active Directory uuid'
541+
EQUALITY octetStringMatch
542+
SYNTAX 1.3.6.1.4.1.1466.115.121.1.40
543+
SINGLE-VALUE )
544+
olcAttributeTypes: ( 1.2.840.113556.1.4.146
545+
NAME 'objectSid'
546+
DESC 'Microsoft Active Directory SID'
547+
EQUALITY octetStringMatch
548+
SYNTAX 1.3.6.1.4.1.1466.115.121.1.40
549+
SINGLE-VALUE )
550+
olcObjectClasses: ( 1.2.3.4.56789.1.0.200
551+
NAME 'mockAdAttributes'
552+
SUP inetOrgPerson
553+
STRUCTURAL
554+
MAY ( objectGUID $ objectSid ) )
555+
556+
557+
dn: ou=TestAdObjects,dc=example,dc=org
558+
objectClass: organizationalUnit
559+
ou: TestAdObjects
560+
561+
dn: ou=People,ou=TestAdObjects,dc=example,dc=org
562+
objectClass: organizationalUnit
563+
ou: People
564+
565+
dn: cn=jane,ou=People,ou=TestAdObjects,dc=example,dc=org
566+
objectClass: person
567+
objectClass: inetOrgPerson
568+
objectClass: top
569+
objectClass: mockAdAttributes
570+
sn: doe
571+
cn: jane
572+
mail: janedoe@example.com
573+
userpassword: foo
574+
objectSid:: AQUAAAAAAAUVAAAA3PTcO4M9K0aCi6Yo0gQAAA==
575+
objectGUID:: Ej5FZ+ibEtOkVkJmFBdAAA==

0 commit comments

Comments
 (0)