diff --git a/ds.go b/ds.go index 124097a..6e6111b 100644 --- a/ds.go +++ b/ds.go @@ -1,5 +1,11 @@ package schemax +//import "fmt" + +/* +ds.go contains all DIT structure rule related methods and functions. +*/ + /* NewDITStructureRules initializes a new [DITStructureRules] instance. */ @@ -598,8 +604,12 @@ func (r *dITStructureRule) setRuleID(x any) { } /* -SetSuperRule assigns the provided input [DITStructureRule] instance(s) to the -receiver's SUP clause. +SetSuperRule assigns the provided input [DITStructureRule] instance(s) +to the receiver's SUP clause. + +If the input arguments contain the `self` special keyword, the receiver +instance will be added to the underlying instance of [DITStructureRules]. +This is meant to allow recursive (self-referencing) rules. This is a fluent method. */ @@ -612,16 +622,22 @@ func (r DITStructureRule) SetSuperRule(m ...any) DITStructureRule { } func (r *dITStructureRule) setSuperRule(m ...any) { - var err error - for i := 0; i < len(m) && err == nil; i++ { + for i := 0; i < len(m); i++ { var def DITStructureRule switch tv := m[i].(type) { - case string: + case uint64, uint, int: def = r.schema.DITStructureRules().get(tv) + case string: + if lc(tv) == `self` { + // handle recursive reference + def = DITStructureRule{r} + } else { + def = r.schema.DITStructureRules().get(tv) + } case DITStructureRule: def = tv default: - err = ErrInvalidType + continue } r.SuperRules.Push(def) @@ -773,6 +789,124 @@ func (r DITStructureRule) String() (dsr string) { return } +/* +Govern returns an error following an analysis of the input dn string +value. + +The analysis of the DN will verify whether the RDN component complies +with the receiver instance. + +If the receiver instance is subordinate to a superior structure rule, +the parent RDN -- if present in the DN -- shall be similarly analyzed. +The process continues throughout the entire structure rule "chain". A +DN must comply with ALL rules in a particular chain in order to "pass". + +The flat integer value describes the number of commas (starting from +the far right) to IGNORE during the delimitation process. This allows +for so-called "flattened root suffix" values, e.g.: "dc=example,dc=com", +to be identified, thus avoiding WRONGFUL delimitation to "dc=example" +AND "dc=com" as separate and distinct entries. + +Please note this is a mock model of the analyses which compatible +directory products shall execute. Naturally, there is no database (DIT) +thus it is only a measure of the full breadth of structure rule checks. +*/ +func (r DITStructureRule) Govern(dn string, flat ...int) (err error) { + if r.IsZero() { + err = ErrNilReceiver + return + } + + gdn := tokenizeDN(dn, flat...) + if gdn.isZero() { + err = ErrInvalidDNOrFlatInt + return + } + + rdn := gdn.components[0] + + var mok int + var moks []string + + // gather name form components + must := r.Form().Must() + may := r.Form().May() + noc := r.Form().OC() // named object class + + // Iterate each ATV within the RDN. + for i := 0; i < len(rdn); i++ { + atv := rdn[i] + at := atv[0] // attribute type + + if sch := r.Schema(); !sch.IsZero() { + // every attribute type must be + // present within the underlying + // schema (when non-zero) ... + if !sch.AttributeTypes().Contains(at) { + err = ErrAttributeTypeNotFound + return + } + } + + // Make sure the named object class (i.e.: the + // STRUCTURAL class present in the receiver's + // name form "OC" clause) facilitates the type + // in some way. + if !(noc.Must().Contains(at) || noc.May().Contains(at)) { + err = ErrNamingViolationBadClassAttr + return + } + + if must.Contains(at) { + if !strInSlice(at, moks) { + mok++ + moks = append(moks, at) + } + } else if !may.Contains(at) { + err = ErrNamingViolationUnsanctioned + return + } + } + + // NO required RDN types were satisfied. + if mok == 0 { + err = ErrNamingViolationMissingMust + return + } + + // If there are no errors AND there are super rules, + // try to find the right rule chain to follow. + err = r.governRecurse(gdn, flat...) + + return +} + +func (r DITStructureRule) governRecurse(gdn *governedDistinguishedName, flat ...int) (err error) { + sr := r.SuperRules() + + if len(gdn.components) > 1 && sr.Len() > 0 { + for i := 0; i < sr.Len(); i++ { + pdn := &governedDistinguishedName{ + components: gdn.components[1:], + flat: gdn.flat, + length: gdn.length - 1, + } + + // Recurse parent DN + if err = sr.Index(i).Govern(detokenizeDN(pdn), flat...); err != nil { + if sr.Len() == i+1 { + break // we failed, and there are no more rules. + } + // we failed, BUT there are more rules to try; continue. + } else { + break // we passed! stop processing. + } + } + } + + return +} + /* Inventory returns an instance of [Inventory] which represents the current inventory of [DITStructureRule] instances within the receiver. @@ -811,7 +945,7 @@ func (r DITStructureRules) iDsStringer(_ ...any) (present string) { padchar = `` } - joined := join(_present, padchar+` `+padchar) + joined := join(_present, padchar) present = `(` + padchar + joined + padchar + `)` } diff --git a/ds_test.go b/ds_test.go index 9b57246..e47747b 100644 --- a/ds_test.go +++ b/ds_test.go @@ -5,6 +5,22 @@ import ( "testing" ) +func ExampleDITStructureRule_Govern() { + dn := `dc=example,dc=com` // flattened context (1 comma) + + // create a new DIT structure rule to leverage + // RFC 2377's 'domainNameForm' definition + dcdsr := mySchema.NewDITStructureRule(). + SetRuleID(13). + SetName(`domainStructureRule`). + SetForm(mySchema.NameForms().Get(`domainNameForm`)). + SetStringer() + + err := dcdsr.Govern(dn, 1) // flat int + fmt.Println(err) + // Output: +} + /* This example demonstrates the creation of a [DITStructureRule]. */ @@ -505,6 +521,66 @@ func ExampleDITStructureRule_Parse_bogus() { // Output: Inconsistent antlr4512.DITStructureRule parse results or bad content } +func TestDITStructureRule_Govern(t *testing.T) { + // create a new DIT structure rule to leverage + // RFC 2377's 'domainNameForm' definition + dcdsr := mySchema.NewDITStructureRule(). + SetRuleID(13). + SetName(`domainStructureRule`). + SetForm(mySchema.NameForms().Get(`domainNameForm`)). + SetStringer() + mySchema.DITStructureRules().Push(dcdsr) + + ounf := mySchema.NewNameForm(). + SetNumericOID(`1.3.6.1.4.1.56521.999.55.11.33`). + SetName(`ouNameForm`). + SetOC(`organizationalUnit`). + SetMust(`ou`). + SetStringer() + mySchema.NameForms().Push(ounf) + + oudsr := mySchema.NewDITStructureRule(). + SetRuleID(14). + SetName(`ouStructureRule`). + SetForm(mySchema.NameForms().Get(`ouNameForm`)). + SetSuperRule(13, `self`). + SetStringer() + + mySchema.DITStructureRules().Push(oudsr) + + for idx, strukt := range []struct { + DN string + L int + ID int + }{ + {`dc=example,dc=com`, 1, 13}, + {`o=example`, 1, 13}, + {`ou=People,dc=example,dc=com`, 1, 14}, + {`ou=People,dc=example,dc=com`, -1, 13}, + {`ou=Employees,ou=People,dc=example,dc=com`, 1, 14}, + {`x=People,dc=example,dc=com`, 1, 14}, + {`ou=People+ou=Employees,dc=example,dc=com`, 1, 14}, + {`ou=People+cn=Employees,dc=example,dc=com`, 1, 14}, + {`ou=People+ou=Employees,dc=example,dc=com`, 1, 14}, + {`ou=Employees,ou=People,dc=example,dc=com`, 1, 13}, + } { + rule := mySchema.DITStructureRules().Get(strukt.ID) + even := idx%2 == 0 + + if err := rule.Govern(strukt.DN, strukt.L); err != nil { + if even { + t.Errorf("%s[%d] failed: %v (%v)", t.Name(), idx, strukt.DN, err) + return + } + } else { + if !even { + t.Errorf("%s[%d] failed: expected error, got nothing", t.Name(), idx) + return + } + } + } +} + /* Do stupid things to make schemax panic, gain additional coverage in the process. @@ -528,6 +604,7 @@ func TestDITStructureRule_codecov(t *testing.T) { r.RuleID() r.setOID(``) r.macro() + r.Govern(`bogusdn`) r.Parse(`crap`) r.IsIdentifiedAs(`nothing`) r.Replace(DITStructureRule{&dITStructureRule{}}) diff --git a/err.go b/err.go index c4d3ae3..33bed58 100644 --- a/err.go +++ b/err.go @@ -8,23 +8,28 @@ for use within this package as well as by end-users writing closures. import "errors" var ( - ErrNilSyntaxQualifier error = errors.New("No SyntaxQualifier instance assigned to LDAPSyntax") - ErrNilValueQualifier error = errors.New("No ValueQualifier instance assigned to AttributeType") - ErrNilAssertionMatcher error = errors.New("No AssertionMatcher instance assigned to MatchingRule") - ErrNilReceiver error = errors.New("Receiver instance is nil") - ErrNilInput error = errors.New("Input instance is nil") - ErrNilDef error = errors.New("Referenced definition is nil or not specified") - ErrNilSchemaRef error = errors.New("Receiver instance lacks a Schema reference") - ErrDefNonCompliant error = errors.New("Definition failed compliancy checks") - ErrInvalidInput error = errors.New("Input instance not compatible") - ErrInvalidSyntax error = errors.New("Value does not meet the prescribed syntax qualifications") - ErrInvalidValue error = errors.New("Value does not meet the prescribed value qualifications") - ErrNoMatch error = errors.New("Values do not match according to prescribed assertion match") - ErrInvalidType error = errors.New("Incompatible type for operation") - ErrTypeAssert error = errors.New("Type assertion failed") - ErrNotUnique error = errors.New("Definition is already defined") - ErrNotEqual error = errors.New("Values are not equal") - ErrMissingNumericOID error = errors.New("Missing or invalid numeric OID for definition") + ErrNamingViolationMissingMust error = errors.New("Naming violation; required attribute type not used") + ErrNamingViolationUnsanctioned error = errors.New("Naming violation; unsanctioned attribute type used") + ErrNamingViolationChildlessSSR error = errors.New("Naming violation; childless superior structure rule") + ErrNamingViolationBadClassAttr error = errors.New("Naming violation; named object class does not facilitate one or more attribute types present") + ErrNilSyntaxQualifier error = errors.New("No SyntaxQualifier instance assigned to LDAPSyntax") + ErrNilValueQualifier error = errors.New("No ValueQualifier instance assigned to AttributeType") + ErrNilAssertionMatcher error = errors.New("No AssertionMatcher instance assigned to MatchingRule") + ErrNilReceiver error = errors.New("Receiver instance is nil") + ErrNilInput error = errors.New("Input instance is nil") + ErrNilDef error = errors.New("Referenced definition is nil or not specified") + ErrNilSchemaRef error = errors.New("Receiver instance lacks a Schema reference") + ErrDefNonCompliant error = errors.New("Definition failed compliancy checks") + ErrInvalidInput error = errors.New("Input instance not compatible") + ErrInvalidSyntax error = errors.New("Value does not meet the prescribed syntax qualifications") + ErrInvalidValue error = errors.New("Value does not meet the prescribed value qualifications") + ErrNoMatch error = errors.New("Values do not match according to prescribed assertion match") + ErrInvalidType error = errors.New("Incompatible type for operation") + ErrTypeAssert error = errors.New("Type assertion failed") + ErrNotUnique error = errors.New("Definition is already defined") + ErrNotEqual error = errors.New("Values are not equal") + ErrMissingNumericOID error = errors.New("Missing or invalid numeric OID for definition") + ErrInvalidDNOrFlatInt error = errors.New("Invalid DN or flattened integer") ErrOrderingRuleNotFound error = errors.New("ORDERING MatchingRule not found") ErrSubstringRuleNotFound error = errors.New("SUBSTR MatchingRule not found") diff --git a/misc.go b/misc.go index 9720c0b..f6b2eb1 100644 --- a/misc.go +++ b/misc.go @@ -195,3 +195,164 @@ func condenseWHSP(b string) (a string) { a = trimS(a) //once more return } + +/* +governedDistinguishedName contains the components of a distinguished +name and the integer length of those components that are not distinct. + +For example, a root suffix of "dc=example,dc=com" has two (2) components, +meaning its flattened integer length is one (1), as "dc=example" and +"dc=com" are not separate and distinct. + +An easier, though less descriptive, explanation of the integer length +is simply "the number of comma characters at the far right (root) of +the DN which do NOT describe separate entries. Again, the comma in the +"dc=example,dc=com" suffix equals a length of one (1). +*/ +type governedDistinguishedName struct { + components [][][]string + flat int + length int +} + +func (r *governedDistinguishedName) isZero() bool { + if r != nil { + return len(r.components) == 0 + } + + return true +} + +/* +tokenizeDN will attempt to tokenize the input dn value. + +Through the act of tokenization, the following occurs: + +An LDAP distinguished name, such as "uid=jesse+gidNumber=5042,ou=People,dc=example,dc=com, + +... is returned as: + + [][][]string{ + [][]string{ + []string{`uid`,`jesse`}, + []string{`gidNumber`,`5042`}, + }, + [][]string{ + []string{`ou`,`People`}, + }, + [][]string{ + []string{`dc`,`example`}, + }, + [][]string{ + []string{`dc`,`com`}, + }, + } + +Please note that this function is NOT considered a true parser. If actual +parsing of component attribute values within a given DN is either desired +or required, consider use of a proper parser such as [go-ldap/v3's ParseDN] +function. + +flat is an integer value that describes the flattened root suffix "length". +For instance, given the root suffix of "dc=example,dc=com" -- which is a +single entry and not two separate entries -- the input value should be the +integer 1. + +[go-ldap/v3's ParseDN]: https://pkg.go.dev/github.com/go-ldap/ldap/v3#ParseDN +*/ +func tokenizeDN(d string, flat ...int) (x *governedDistinguishedName) { + if len(d) == 0 { + return + } + + x = &governedDistinguishedName{ + components: make([][][]string, 0), + } + + if len(flat) > 0 { + x.flat = flat[0] + } + + rdns := splitUnescaped(d, `,`, `\`) + lr := len(rdns) + + if lr == x.flat || x.flat < 0 { + // bogus depth + return + } + + for i := 0; i < lr; i++ { + var atvs [][]string = make([][]string, 0) + srdns := splitUnescaped(rdns[i], `+`, `\`) + for j := 0; j < len(srdns); j++ { + if atv := split(srdns[j], `=`); len(atv) == 2 { + atvs = append(atvs, atv) + } else { + atvs = append(atvs, []string{}) + } + } + + x.components = append(x.components, atvs) + } + + if x.flat > 0 { + e := lr - 1 + f := e - x.flat + x.components[f] = append(x.components[f], x.components[e]...) + x.components = x.components[:e] + } + x.length = len(x.components) + + return +} + +func detokenizeDN(x *governedDistinguishedName) (dtkz string) { + if x.isZero() { + return + } + + var rdns []string + for i := 0; i < x.length; i++ { + rdn := x.components[i] + char := `,` + if i < x.length-x.flat && len(rdn) > 1 { + char = `+` + } + + //var r []string + var atv []string + for j := 0; j < len(rdn); j++ { + atv = append(atv, rdn[j][0]+`=`+rdn[j][1]) + } + rdns = append(rdns, join(atv, char)) + } + + dtkz = join(rdns, `,`) + return +} + +func splitUnescaped(str, sep, esc string) (slice []string) { + slice = split(str, sep) + for i := len(slice) - 2; i >= 0; i-- { + if hasSfx(slice[i], esc) { + slice[i] = slice[i][:len(slice[i])-len(esc)] + sep + slice[i+1] + slice = append(slice[:i+1], slice[i+2:]...) + } + } + + return +} + +/* +strInSlice returns a Boolean value indicative of whether the +specified string (str) is present within slice. Please note +that case is a significant element in the matching process. +*/ +func strInSlice(str string, slice []string) bool { + for i := 0; i < len(slice); i++ { + if str == slice[i] { + return true + } + } + return false +} diff --git a/misc_test.go b/misc_test.go index c40ab7c..324cab8 100644 --- a/misc_test.go +++ b/misc_test.go @@ -31,3 +31,27 @@ dolor sit amet, incididunt ut labore et dolore magna aliqua.`) } + +func TestGovernedDistinguishedName(t *testing.T) { + for _, strukt := range []struct { + DN string + L int + }{ + {`dc=example,dc=com`, 0}, + {`dc=example,dc=com`, 1}, + {`ou=People,dc=example,dc=com`, 1}, + {`ou=People+ou=Employees,dc=example,dc=com`, 1}, + {`o=example`, 0}, + } { + if cdn := tokenizeDN(strukt.DN, strukt.L); cdn.isZero() { + t.Errorf("%s failed: no content", t.Name()) + return + } else { + if dtkx := detokenizeDN(cdn); dtkx != strukt.DN { + t.Errorf("%s failed: want %s, got %s [raw:%#v]", + t.Name(), strukt.DN, dtkx, cdn) + return + } + } + } +} diff --git a/stackage.go b/stackage.go index bae60af..704d929 100644 --- a/stackage.go +++ b/stackage.go @@ -52,7 +52,7 @@ func newRuleIDList(name string) RuleIDList { `stringer`: nil, }). SetCategory(`ruleids`). - SetDelimiter(' '). + SetDelimiter(`_null_`). Paren(true). Mutex()) }