Answer AST name queries for the source fact on /facts
This commit is contained in:
@@ -3,6 +3,8 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -204,3 +206,143 @@ func constrainsField(parts []json.RawMessage, field string) bool {
|
||||
var name string
|
||||
return json.Unmarshal(parts[1], &name) == nil && name == field
|
||||
}
|
||||
|
||||
// sourceQuery returns the parsed /facts query when one of its name comparisons
|
||||
// selects the owned fact and the whole query can be evaluated locally against a
|
||||
// fact record; nil otherwise, which leaves the query on the gated path.
|
||||
// ponytail: a negated name match (["not",["=","name","x"]]) does not select the
|
||||
// owned fact; add three-valued evaluation if a client needs it.
|
||||
func (si *sourceInjector) sourceQuery(query string) []json.RawMessage {
|
||||
if si == nil {
|
||||
return nil
|
||||
}
|
||||
var ast []json.RawMessage
|
||||
if json.Unmarshal([]byte(query), &ast) != nil {
|
||||
return nil
|
||||
}
|
||||
probe := factFields{Name: si.name}
|
||||
if _, ok := matchFact(ast, probe); !ok || !namesFact(ast, probe) {
|
||||
return nil
|
||||
}
|
||||
return ast
|
||||
}
|
||||
|
||||
// factFields are the queryable columns of the facts entity (openvoxdb
|
||||
// engine.clj facts-query). value is a string here: the synthetic value always is.
|
||||
type factFields struct {
|
||||
Certname string `json:"certname"`
|
||||
Environment string `json:"environment"`
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func (f factFields) column(name string) (string, bool) {
|
||||
switch name {
|
||||
case "certname":
|
||||
return f.Certname, true
|
||||
case "environment":
|
||||
return f.Environment, true
|
||||
case "name":
|
||||
return f.Name, true
|
||||
case "value":
|
||||
return f.Value, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// namesFact reports whether a name comparison in the boolean skeleton matches f.
|
||||
func namesFact(parts []json.RawMessage, f factFields) bool {
|
||||
var op, field string
|
||||
if len(parts) < 2 || json.Unmarshal(parts[0], &op) != nil {
|
||||
return false
|
||||
}
|
||||
switch op {
|
||||
case "and", "or", "not":
|
||||
for _, p := range parts[1:] {
|
||||
var sub []json.RawMessage
|
||||
if json.Unmarshal(p, &sub) == nil && namesFact(sub, f) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if json.Unmarshal(parts[1], &field) != nil || field != "name" {
|
||||
return false
|
||||
}
|
||||
match, _ := matchFact(parts, f)
|
||||
return match
|
||||
}
|
||||
|
||||
// matchFact evaluates a facts query against one record with openvoxdb's
|
||||
// semantics for and/or/not, =, ~ and in-array. ok is false for anything else,
|
||||
// including subqueries, so a caller can refuse rather than guess.
|
||||
func matchFact(parts []json.RawMessage, f factFields) (match, ok bool) {
|
||||
var op string
|
||||
if len(parts) == 0 || json.Unmarshal(parts[0], &op) != nil {
|
||||
return false, false
|
||||
}
|
||||
switch op {
|
||||
case "and", "or":
|
||||
match = op == "and"
|
||||
for _, p := range parts[1:] {
|
||||
var sub []json.RawMessage
|
||||
if json.Unmarshal(p, &sub) != nil {
|
||||
return false, false
|
||||
}
|
||||
m, ok := matchFact(sub, f)
|
||||
if !ok {
|
||||
return false, false
|
||||
}
|
||||
if op == "and" {
|
||||
match = match && m
|
||||
} else {
|
||||
match = match || m
|
||||
}
|
||||
}
|
||||
return match, true
|
||||
case "not":
|
||||
var sub []json.RawMessage
|
||||
if len(parts) != 2 || json.Unmarshal(parts[1], &sub) != nil {
|
||||
return false, false
|
||||
}
|
||||
m, ok := matchFact(sub, f)
|
||||
return !m, ok
|
||||
}
|
||||
var field string
|
||||
if len(parts) != 3 || json.Unmarshal(parts[1], &field) != nil {
|
||||
return false, false
|
||||
}
|
||||
got, known := f.column(field)
|
||||
if !known {
|
||||
return false, false
|
||||
}
|
||||
switch op {
|
||||
case "=":
|
||||
var want any
|
||||
if json.Unmarshal(parts[2], &want) != nil {
|
||||
return false, false
|
||||
}
|
||||
return want == any(got), true
|
||||
case "~":
|
||||
var pattern string
|
||||
if json.Unmarshal(parts[2], &pattern) != nil {
|
||||
return false, false
|
||||
}
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return false, false
|
||||
}
|
||||
return re.MatchString(got), true
|
||||
case "in":
|
||||
var arr []json.RawMessage
|
||||
var tag string
|
||||
var values []any
|
||||
if json.Unmarshal(parts[2], &arr) != nil || len(arr) != 2 ||
|
||||
json.Unmarshal(arr[0], &tag) != nil || tag != "array" ||
|
||||
json.Unmarshal(arr[1], &values) != nil {
|
||||
return false, false
|
||||
}
|
||||
return slices.Contains(values, any(got)), true
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user