Initial import — snapshot from admin host /srv/gosec/gsc-ops-api
This repo had no version control prior to this commit. The import is a
straight snapshot of the working tree at 2026-05-03; the deployed
binary on fihelvop01 was being rebuilt from this source via `make
build` + scp into place, with no upstream review path.
The snapshot already includes one in-flight fix made on 2026-05-03 to
internal/service/persona.go:GetSelfModel — the handler queried
`source` and `strength` columns plus an `is_active = true` filter on
persona.persona_commitments, none of which exist on that table (its
shape is session-bound commitments with `status`, `commitment_meta`,
etc.). The query returned a 500 every time SynapseHub bootstrapped a
persona's self-model, dropping the IdentityConstraints / Commitments /
ConscienceStandards layer from the assembled prompt. The patched
query reads existing columns only (commitment_text, commitment_type),
filters on `status='active'`, and synthesises Source="learned" /
Strength=1.0 to keep the SelfModel response shape stable for callers.
Verified live: `GET /api/v1/personas/70f7cfd9-.../self-model` now
returns 200 with `{identityConstraints:[],commitments:[],
conscienceStandards:[]}` instead of 500.
Future changes go through PRs against this repo — no more bin-only
deploys.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
273
internal/schema/registry.go
Normal file
273
internal/schema/registry.go
Normal file
@@ -0,0 +1,273 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user