Files
Alphaeus Mote 6587905eeb fix(ldap): decode objectGUID/objectSid and binary attributes for display
The object viewer showed raw bytes for binary attributes. Format them for
display: objectGUID as a canonical GUID, objectSid/sIDHistory as S-1-… SID
strings, and any other non-printable value as base64. Applied in the
query-preview path that the object viewer uses; printable values pass through.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 22:01:34 -04:00

110 lines
2.9 KiB
Go

// Package ldap - human-readable formatting for binary directory attributes.
package ldap
import (
"encoding/base64"
"fmt"
"strings"
"unicode/utf8"
goldap "github.com/go-ldap/ldap/v3"
)
// binaryAttrsGUID / binaryAttrsSID name the well-known AD attributes stored as
// raw binary that must be decoded to be legible.
var (
guidAttrs = map[string]bool{"objectguid": true}
sidAttrs = map[string]bool{"objectsid": true, "sidhistory": true}
)
// FormatAttributeValues returns display-friendly string values for an attribute:
// objectGUID as a GUID, objectSid/sIDHistory as S-1-… strings, and any other
// value that isn't valid printable UTF-8 as base64. Printable values pass
// through unchanged.
func FormatAttributeValues(attr *goldap.EntryAttribute) []string {
name := strings.ToLower(attr.Name)
if guidAttrs[name] {
out := make([]string, 0, len(attr.ByteValues))
for _, b := range attr.ByteValues {
out = append(out, formatGUID(b))
}
if len(out) > 0 {
return out
}
}
if sidAttrs[name] {
out := make([]string, 0, len(attr.ByteValues))
for _, b := range attr.ByteValues {
out = append(out, formatSID(b))
}
if len(out) > 0 {
return out
}
}
// Generic: keep printable text, base64 anything binary.
out := make([]string, len(attr.Values))
for i, v := range attr.Values {
if isPrintable(v) {
out[i] = v
} else if i < len(attr.ByteValues) {
out[i] = "base64:" + base64.StdEncoding.EncodeToString(attr.ByteValues[i])
} else {
out[i] = "base64:" + base64.StdEncoding.EncodeToString([]byte(v))
}
}
return out
}
// formatGUID renders a 16-byte AD objectGUID as its canonical string, honoring
// the mixed-endian layout of the first three groups.
func formatGUID(b []byte) string {
if len(b) != 16 {
return "base64:" + base64.StdEncoding.EncodeToString(b)
}
return fmt.Sprintf("%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
b[3], b[2], b[1], b[0], b[5], b[4], b[7], b[6],
b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15])
}
// formatSID renders a binary NT security identifier as S-R-IA-SA1-SA2-…
func formatSID(b []byte) string {
if len(b) < 8 {
return "base64:" + base64.StdEncoding.EncodeToString(b)
}
revision := b[0]
subCount := int(b[1])
// Identifier authority: 48-bit big-endian in bytes 2..7.
var authority uint64
for i := 2; i < 8; i++ {
authority = authority<<8 | uint64(b[i])
}
sid := fmt.Sprintf("S-%d-%d", revision, authority)
// Sub-authorities: 32-bit little-endian, subCount of them.
for i := 0; i < subCount; i++ {
off := 8 + i*4
if off+4 > len(b) {
break
}
sub := uint32(b[off]) | uint32(b[off+1])<<8 | uint32(b[off+2])<<16 | uint32(b[off+3])<<24
sid += fmt.Sprintf("-%d", sub)
}
return sid
}
func isPrintable(s string) bool {
if !utf8.ValidString(s) {
return false
}
for _, r := range s {
if r == '\t' || r == '\n' || r == '\r' {
continue
}
if r < 0x20 {
return false
}
}
return true
}