6587905eeb
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>
110 lines
2.9 KiB
Go
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
|
|
}
|