diff --git a/src/schema.go b/src/schema.go index 3f571cc..07d558c 100644 --- a/src/schema.go +++ b/src/schema.go @@ -1,144 +1,180 @@ -package main - -import ( - "context" - "errors" - "encoding/json" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strings" - - "github.com/paranoidguy/jsonschema" - jsonpatch "github.com/evanphx/json-patch" - jptr "github.com/qri-io/jsonpointer" -) - -var userSchema *jsonschema.Schema - -// our custom validator -type IsLocked 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 := ioutil.ReadFile(fileSchema) - if err != nil { - return err - } - rs := &jsonschema.Schema{} - jsonschema.LoadDraft2019_09() - jsonschema.RegisterKeyword("locked", newIsLocked) - err = rs.UnmarshalJSON(schemaData) - if err != nil { - return err - } - userSchema = rs - return nil -} - -func ValidateUserEnabled() 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) error { - if userSchema == nil { - return nil - } - var oldDoc interface{} - var newDoc interface{} - if err := json.Unmarshal(oldRecord, &oldDoc); err != nil { - return err - } - if err := json.Unmarshal(newRecord, &newDoc); err != nil { - return 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 (*result.Errs)[0] - } - for _, r := range *result.ExtendedResults { - fmt.Printf("path: %s key: %s data: %v\n", r.PropertyPath, r.Key, r.Value) - if r.Key == "locked" { - 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) { - fmt.Printf("Locked value changed. Old: %s New %s\n", data1Binary, data2Binary) - return errors.New("User schema check error. Locked value changed: "+r.PropertyPath) - } - } - } - - return nil -} -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) { - fmt.Printf("Register %s\n", uri) -} - -// Resolve implements jsonschema.Keyword -func (f *IsLocked) Resolve(pointer jptr.Pointer, uri string) *jsonschema.Schema { - fmt.Printf("Resolve %s\n", uri) - return nil -} - -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) -} +package main + +import ( + "context" + "errors" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/paranoidguy/jsonschema" + jsonpatch "github.com/evanphx/json-patch" + jptr "github.com/qri-io/jsonpointer" +) + +var userSchema *jsonschema.Schema + +// our custom validator +type IsLocked bool +type IsAdmin 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 := ioutil.ReadFile(fileSchema) + if err != nil { + return err + } + rs := &jsonschema.Schema{} + jsonschema.LoadDraft2019_09() + jsonschema.RegisterKeyword("locked", newIsLocked) + jsonschema.RegisterKeyword("admin", newIsAdmin) + err = rs.UnmarshalJSON(schemaData) + if err != nil { + return err + } + userSchema = rs + return nil +} + +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") { + 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) + } else { + fmt.Printf("Admin value changed. Approval required. Old: %s New %s\n", data1Binary, data2Binary) + adminRecordChanged = true + } + } + } + } + return adminRecordChanged, nil +} + +// Locked keyword - meaningin that value should never be changed after record created +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 +} + +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) +} + +// 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 +} + +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) +} diff --git a/src/users_api.go b/src/users_api.go index 068f46c..b76b517 100644 --- a/src/users_api.go +++ b/src/users_api.go @@ -189,8 +189,9 @@ func (e mainEnv) userChange(w http.ResponseWriter, r *http.Request, ps httproute if authResult == "" { return } - if ValidateUserEnabled() { - err = e.db.validateUserRecordChange(parsedData.jsonData, userTOKEN) + adminRecordChanged := false + if UserSchemaEnabled() { + adminRecordChanged, err = e.db.validateUserRecordChange(parsedData.jsonData, userTOKEN, authResult) if err != nil { returnError(w, r, "schema validation error: " + err.Error(), 405, err, event) return @@ -198,7 +199,7 @@ func (e mainEnv) userChange(w http.ResponseWriter, r *http.Request, ps httproute } if authResult == "login" { event.Title = "user change-profile request" - if e.conf.SelfService.UserRecordChange == false { + if e.conf.SelfService.UserRecordChange == false || adminRecordChanged == true { rtoken, err := e.db.saveUserRequest("change-profile", userTOKEN, "", "", parsedData.jsonData) if err != nil { returnError(w, r, "internal error", 405, err, event) diff --git a/src/users_db.go b/src/users_db.go index fff2259..d49f9c1 100644 --- a/src/users_db.go +++ b/src/users_db.go @@ -88,40 +88,36 @@ func (dbobj dbcon) generateDemoLoginCode(userTOKEN string) int32 { return rnd } -func (dbobj dbcon) validateUserRecordChange(jsonDataPatch []byte, userTOKEN string) error { +func (dbobj dbcon) validateUserRecordChange(jsonDataPatch []byte, userTOKEN string, authResult string) (bool, error) { oldUserBson, err := dbobj.lookupUserRecord(userTOKEN) if oldUserBson == nil || err != nil { // not found - return errors.New("not found") + return false, errors.New("not found") } // get user key userKey := oldUserBson["key"].(string) recordKey, err := base64.StdEncoding.DecodeString(userKey) if err != nil { - return err + return false, err } encData0 := oldUserBson["data"].(string) encData, err := base64.StdEncoding.DecodeString(encData0) if err != nil { - return err + return false, err } decrypted, err := decrypt(dbobj.masterKey, recordKey, encData) if err != nil { - return err + return false, err } // prepare merge fmt.Printf("old json: %s\n", decrypted) fmt.Printf("json patch: %s\n", jsonDataPatch) newJSON, err := jsonpatch.MergePatch(decrypted, jsonDataPatch) if err != nil { - return err + return false, err } fmt.Printf("result: %s\n", newJSON) - err = ValidateUserRecordChange(decrypted, newJSON) - if err != nil { - return err - } - return nil + return ValidateUserRecordChange(decrypted, newJSON, authResult) } func (dbobj dbcon) updateUserRecord(jsonDataPatch []byte, userTOKEN string, event *auditEvent, conf Config) ([]byte, []byte, bool, error) { diff --git a/user.json b/user.json index d1fc95c..1797faa 100644 --- a/user.json +++ b/user.json @@ -23,11 +23,13 @@ "email": { "type": "string", "minLength": 1, - "format": "email" + "format": "email", + "admin": true }, "phone": { "type": "string", - "minLength": 1 + "minLength": 1, + "admin": true }, "status": { "type": "string",