Files
databunker/src/schema.go
2024-12-28 21:39:51 +02:00

321 lines
8.8 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
jsonpatch "github.com/evanphx/json-patch"
jptr "github.com/qri-io/jsonpointer"
"github.com/securitybunker/jsonschema"
)
var userSchema *jsonschema.Schema
// IsAdmin - admin/DPO approval is required
type IsAdmin bool
// IsLocked - variable is locked from changes
type IsLocked bool
// IsPreserve variable can never be deleted
type IsPreserve bool
func loadUserSchema(cfg Config, confFile *string) error {
fileSchema := cfg.Generic.UserRecordSchema
parentDir := ""
if confFile != nil && len(*confFile) > 0 {
parentDir = filepath.Base(*confFile)
if parentDir != "." {
parentDir = ""
}
}
if len(fileSchema) == 0 {
return nil
}
if strings.HasPrefix(fileSchema, "./") {
_, err := os.Stat(cfg.Generic.UserRecordSchema)
if os.IsNotExist(err) && confFile != nil {
fileSchema = parentDir + fileSchema[2:]
}
} else {
fileSchema = parentDir + fileSchema
}
_, err := os.Stat(fileSchema)
if os.IsNotExist(err) {
return err
}
schemaData, err := os.ReadFile(fileSchema)
if err != nil {
return err
}
rs := &jsonschema.Schema{}
jsonschema.LoadDraft2019_09()
jsonschema.RegisterKeyword("admin", newIsAdmin)
jsonschema.RegisterKeyword("locked", newIsLocked)
jsonschema.RegisterKeyword("preserve", newIsPreserve)
err = rs.UnmarshalJSON(schemaData)
if err != nil {
return err
}
userSchema = rs
return nil
}
// UserSchemaEnabled function checks is user schema is defined
func UserSchemaEnabled() bool {
if userSchema == nil {
return false
}
return true
}
func validateUserRecord(record []byte) error {
if userSchema == nil {
return nil
}
var doc interface{}
if err := json.Unmarshal(record, &doc); err != nil {
return err
}
result := userSchema.Validate(nil, doc)
if len(*result.Errs) > 0 {
return (*result.Errs)[0]
}
return nil
}
func validateUserRecordChange(oldRecord []byte, newRecord []byte, authResult string) (bool, error) {
if userSchema == nil {
return false, nil
}
var oldDoc interface{}
var newDoc interface{}
if err := json.Unmarshal(oldRecord, &oldDoc); err != nil {
return false, err
}
if err := json.Unmarshal(newRecord, &newDoc); err != nil {
return false, err
}
result := userSchema.Validate(nil, newDoc)
//if len(*result.Errs) > 0 {
// return (*result.Errs)[0]
//}
result2 := userSchema.Validate(nil, oldDoc)
if len(*result2.Errs) > 0 {
return false, (*result.Errs)[0]
}
if result.ExtendedResults == nil {
return false, nil
}
adminRecordChanged := false
for _, r := range *result.ExtendedResults {
fmt.Printf("path: %s key: %s data: %v\n", r.PropertyPath, r.Key, r.Value)
if r.Key == "locked" || (r.Key == "admin" && authResult == "login" && adminRecordChanged == false) {
pointer, _ := jptr.Parse(r.PropertyPath)
data1, _ := pointer.Eval(oldDoc)
data1Binary, _ := json.Marshal(data1)
data2, _ := pointer.Eval(newDoc)
data2Binary, _ := json.Marshal(data2)
if !jsonpatch.Equal(data1Binary, data2Binary) {
if r.Key == "locked" {
fmt.Printf("Locked value changed. Old: %s New %s\n", data1Binary, data2Binary)
return false, errors.New("User schema check error. Locked value changed: " + r.PropertyPath)
}
fmt.Printf("Admin value changed. Approval required. Old: %s New %s\n", data1Binary, data2Binary)
adminRecordChanged = true
}
}
}
return adminRecordChanged, nil
}
func cleanupRecord(record []byte) ([]byte, map[string]interface{}) {
if userSchema == nil {
return nil, nil
}
var doc interface{}
if err := json.Unmarshal(record, &doc); err != nil {
return nil, nil
}
result := userSchema.Validate(nil, doc)
if result.ExtendedResults == nil {
return nil, nil
}
doc1 := make(map[string]interface{})
doc2 := make([]interface{}, 1)
nested := func(path string, data interface{}) {
currentStr := &doc1
currentNum := &doc2
keys := strings.Split(path, "/")
fmt.Printf("path: %s\n", path)
for i, k := range keys {
if len(k) == 0 {
continue
}
if kNum, err := strconv.Atoi(k); err == nil {
if (i + 1) == len(keys) {
(*currentNum)[kNum] = data
} else if kNxt, err := strconv.Atoi(keys[i+1]); err == nil {
if (*currentNum)[kNum] == nil {
v := make([]interface{}, kNxt+1)
(*currentNum)[kNum] = v
currentNum = &v
} else {
v := (*currentNum)[kNum].([]interface{})
for len(v) < kNxt+1 {
v = append(v, nil)
}
(*currentNum)[kNum] = v
currentNum = &v
}
} else {
if (*currentNum)[kNum] == nil {
v := make(map[string]interface{})
(*currentNum)[kNum] = v
currentStr = &v
} else {
v := (*currentNum)[kNum].(map[string]interface{})
currentStr = &v
}
}
} else {
if (i + 1) == len(keys) {
(*currentStr)[k] = data
} else if kNxt, err := strconv.Atoi(keys[i+1]); err == nil {
if _, ok := (*currentStr)[k]; !ok {
v := make([]interface{}, kNxt+1)
(*currentStr)[k] = v
currentNum = &v
} else {
v := (*currentStr)[k].([]interface{})
for len(v) < kNxt+1 {
v = append(v, nil)
}
(*currentStr)[k] = v
currentNum = &v
}
} else {
if _, ok := (*currentStr)[k]; !ok {
v := make(map[string]interface{})
(*currentStr)[k] = v
currentStr = &v
} else {
v := (*currentNum)[kNum].(map[string]interface{})
currentStr = &v
}
}
}
}
}
found := false
for _, r := range *result.ExtendedResults {
fmt.Printf("path: %s key: %s data: %v\n", r.PropertyPath, r.Key, r.Value)
if r.Key == "preserve" {
//pointer, _ := jptr.Parse(r.PropertyPath)
//data1, _ := pointer.Eval(oldDoc)
nested(r.PropertyPath, r.Value)
found = true
}
}
if found == false {
return nil, nil
}
//fmt.Printf("final doc1 %v\n", doc1)
dataBinary, _ := json.Marshal(doc1)
//fmt.Println(err)
fmt.Printf("data bin %s\n", dataBinary)
return dataBinary, doc1
}
/*******************************************************************/
// Admin keyword. Any change in this record requires admin approval.
func newIsAdmin() jsonschema.Keyword {
return new(IsAdmin)
}
// Validate implements jsonschema.Keyword
func (f *IsAdmin) Validate(propPath string, data interface{}, errs *[]jsonschema.KeyError) {
fmt.Printf("Validate: %s -> %v\n", propPath, data)
}
// Register implements jsonschema.Keyword
func (f *IsAdmin) Register(uri string, registry *jsonschema.SchemaRegistry) {
}
// Resolve implements jsonschema.Keyword
func (f *IsAdmin) Resolve(pointer jptr.Pointer, uri string) *jsonschema.Schema {
fmt.Printf("Resolve %s\n", uri)
return nil
}
// ValidateKeyword adds admin keyword
func (f *IsAdmin) ValidateKeyword(ctx context.Context, currentState *jsonschema.ValidationState, data interface{}) {
//fmt.Printf("ValidateKeyword admin %s => %v\n", currentState.InstanceLocation.String(), data)
currentState.AddExtendedResult("admin", data)
}
/*******************************************************************/
// Locked keyword - meaningin that value should never be changed after record creation
func newIsLocked() jsonschema.Keyword {
return new(IsLocked)
}
// Validate implements jsonschema.Keyword
func (f *IsLocked) Validate(propPath string, data interface{}, errs *[]jsonschema.KeyError) {
fmt.Printf("Validate: %s -> %v\n", propPath, data)
}
// Register implements jsonschema.Keyword
func (f *IsLocked) Register(uri string, registry *jsonschema.SchemaRegistry) {
}
// Resolve implements jsonschema.Keyword
func (f *IsLocked) Resolve(pointer jptr.Pointer, uri string) *jsonschema.Schema {
fmt.Printf("Resolve %s\n", uri)
return nil
}
// ValidateKeyword adds locked keyword
func (f *IsLocked) ValidateKeyword(ctx context.Context, currentState *jsonschema.ValidationState, data interface{}) {
//fmt.Printf("ValidateKeyword locked %s => %v\n", currentState.InstanceLocation.String(), data)
currentState.AddExtendedResult("locked", data)
}
/*******************************************************************/
// Preserve keyword - meaning that value should never be deleted (after user is delete it is left)
func newIsPreserve() jsonschema.Keyword {
return new(IsPreserve)
}
// Validate implements jsonschema.Keyword
func (f *IsPreserve) Validate(propPath string, data interface{}, errs *[]jsonschema.KeyError) {
fmt.Printf("Validate: %s -> %v\n", propPath, data)
}
// Register implements jsonschema.Keyword
func (f *IsPreserve) Register(uri string, registry *jsonschema.SchemaRegistry) {
}
// Resolve implements jsonschema.Keyword
func (f *IsPreserve) Resolve(pointer jptr.Pointer, uri string) *jsonschema.Schema {
fmt.Printf("Resolve %s\n", uri)
return nil
}
// ValidateKeyword adds preserve keyword
func (f *IsPreserve) ValidateKeyword(ctx context.Context, currentState *jsonschema.ValidationState, data interface{}) {
//fmt.Printf("ValidateKeyword preserve %s => %v\n", currentState.InstanceLocation.String(), data)
currentState.AddExtendedResult("preserve", data)
}