package schema import ( "fmt" "strconv" "strings" "time" ) // Registry is the central schema registry for all GoSec LDAP attributes, // objectClasses, and entity types. type Registry struct { attrs map[string]*AttrDef // ldapName → AttrDef attrsByJSON map[string]*AttrDef // "domain:jsonName" → AttrDef domainAttrs map[string][]*AttrDef // domain → list of attrs objectClasses map[string]*ObjectClassDef // OC name → ObjectClassDef domainOC map[string]string // domain → auxiliary user OC name entityTypes map[string]*EntityTypeDef // entity name → EntityTypeDef } // NewRegistry creates and populates the schema registry func NewRegistry() *Registry { r := &Registry{ attrs: make(map[string]*AttrDef), attrsByJSON: make(map[string]*AttrDef), domainAttrs: make(map[string][]*AttrDef), objectClasses: make(map[string]*ObjectClassDef), domainOC: make(map[string]string), entityTypes: make(map[string]*EntityTypeDef), } r.registerAttributes() r.registerObjectClasses() r.registerEntities() return r } func (r *Registry) addAttr(ldapName, jsonName string, typ AttrType, domain string, readOnly bool) { def := &AttrDef{ LDAPName: ldapName, JSONName: jsonName, Type: typ, Domain: domain, ReadOnly: readOnly, } r.attrs[ldapName] = def r.attrsByJSON[domain+":"+jsonName] = def r.domainAttrs[domain] = append(r.domainAttrs[domain], def) } func (r *Registry) addObjectClass(name, kind string, must, may []string, domain string) { r.objectClasses[name] = &ObjectClassDef{ Name: name, Kind: kind, Must: must, May: may, Domain: domain, } // Map domain → auxiliary user objectClass (first AUXILIARY wins) if kind == "AUXILIARY" { if _, exists := r.domainOC[domain]; !exists { r.domainOC[domain] = name } } } func (r *Registry) addEntityType(name, description string, objectClasses []string, baseDN, rdnAttr, searchFilter, domain string, requiredAttrs []string) { r.entityTypes[name] = &EntityTypeDef{ Name: name, Description: description, ObjectClasses: objectClasses, BaseDN: baseDN, RDNAttribute: rdnAttr, SearchFilter: searchFilter, Domain: domain, RequiredAttrs: requiredAttrs, } } // GetAttr returns an attribute definition by LDAP name func (r *Registry) GetAttr(ldapName string) *AttrDef { return r.attrs[ldapName] } // GetAttrByJSON returns an attribute definition by domain and JSON name func (r *Registry) GetAttrByJSON(domain, jsonName string) *AttrDef { return r.attrsByJSON[domain+":"+jsonName] } // AttrsForDomain returns all attribute definitions for a domain func (r *Registry) AttrsForDomain(domain string) []*AttrDef { return r.domainAttrs[domain] } // AllDomains returns all registered domain names func (r *Registry) AllDomains() []string { domains := make([]string, 0, len(r.domainAttrs)) for d := range r.domainAttrs { domains = append(domains, d) } return domains } // AllUserAttrs returns all gsc* LDAP attribute names for user search func (r *Registry) AllUserAttrs() []string { attrs := make([]string, 0, len(r.attrs)) for name := range r.attrs { attrs = append(attrs, name) } return attrs } // UserOCForDomain returns the auxiliary objectClass name for a user service domain func (r *Registry) UserOCForDomain(domain string) string { return r.domainOC[domain] } // RequiredOCsForAttrs determines which objectClasses are needed for a set of LDAP attributes func (r *Registry) RequiredOCsForAttrs(ldapAttrNames []string) []string { needed := make(map[string]bool) attrSet := make(map[string]bool, len(ldapAttrNames)) for _, a := range ldapAttrNames { attrSet[a] = true } for _, oc := range r.objectClasses { if oc.Kind != "AUXILIARY" { continue } for _, must := range oc.Must { if attrSet[must] { needed[oc.Name] = true break } } if needed[oc.Name] { continue } for _, may := range oc.May { if attrSet[may] { needed[oc.Name] = true break } } } result := make([]string, 0, len(needed)) for name := range needed { result = append(result, name) } return result } // GetObjectClass returns an objectClass definition by name func (r *Registry) GetObjectClass(name string) *ObjectClassDef { return r.objectClasses[name] } // LDAPValueToGo converts LDAP string values to Go typed values based on attribute type func (r *Registry) LDAPValueToGo(attr *AttrDef, values []string) interface{} { if len(values) == 0 { return nil } switch attr.Type { case AttrString, AttrDN: return values[0] case AttrStringMulti, AttrDNMulti: return values case AttrInt: if v, err := strconv.Atoi(values[0]); err == nil { return v } return values[0] case AttrBool: return strings.EqualFold(values[0], "TRUE") case AttrTime: // GeneralizedTime format: 20060102150405Z if t, err := time.Parse("20060102150405Z", values[0]); err == nil { return t.Format(time.RFC3339) } return values[0] default: return values[0] } } // GoValueToLDAP converts a Go value to LDAP string(s) based on attribute type func (r *Registry) GoValueToLDAP(attr *AttrDef, value interface{}) ([]string, error) { if value == nil { return nil, nil } switch attr.Type { case AttrString, AttrDN: s, ok := value.(string) if !ok { return nil, fmt.Errorf("attribute %s expects string, got %T", attr.LDAPName, value) } return []string{s}, nil case AttrStringMulti, AttrDNMulti: switch v := value.(type) { case []string: return v, nil case []interface{}: result := make([]string, 0, len(v)) for _, item := range v { s, ok := item.(string) if !ok { return nil, fmt.Errorf("attribute %s expects string array, got %T in array", attr.LDAPName, item) } result = append(result, s) } return result, nil default: return nil, fmt.Errorf("attribute %s expects string array, got %T", attr.LDAPName, value) } case AttrInt: switch v := value.(type) { case float64: return []string{strconv.Itoa(int(v))}, nil case int: return []string{strconv.Itoa(v)}, nil case string: return []string{v}, nil default: return nil, fmt.Errorf("attribute %s expects int, got %T", attr.LDAPName, value) } case AttrBool: switch v := value.(type) { case bool: if v { return []string{"TRUE"}, nil } return []string{"FALSE"}, nil case string: return []string{strings.ToUpper(v)}, nil default: return nil, fmt.Errorf("attribute %s expects bool, got %T", attr.LDAPName, value) } case AttrTime: s, ok := value.(string) if !ok { return nil, fmt.Errorf("attribute %s expects time string, got %T", attr.LDAPName, value) } // Accept RFC3339 and convert to GeneralizedTime if t, err := time.Parse(time.RFC3339, s); err == nil { return []string{t.UTC().Format("20060102150405Z")}, nil } // Already GeneralizedTime format return []string{s}, nil default: s, ok := value.(string) if !ok { return nil, fmt.Errorf("attribute %s: unsupported type %T", attr.LDAPName, value) } return []string{s}, nil } } // GetEntityType returns an entity type definition by name func (r *Registry) GetEntityType(name string) *EntityTypeDef { return r.entityTypes[name] } // AllEntityTypes returns all registered entity type definitions func (r *Registry) AllEntityTypes() map[string]*EntityTypeDef { return r.entityTypes }