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:
Claude (gsc-ops-api init)
2026-05-03 20:06:02 +02:00
commit 3847eb2036
68 changed files with 12982 additions and 0 deletions

273
internal/schema/registry.go Normal file
View 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
}