Using a custom EFContextProvider, I want to check which properties have been modified on an entity before it saves, so that I can implement:
Security: The client has permission to change only certain properties of an entity.
Auditing: Whenever certain properties are changed, the change needs to be logged.
There are suggestions on SO to use OriginalValuesMap to determine the modified properties, see here and here. If the original value differs from the new value, the property has been modified. However, these original values are supplied by the client, and thus can be forged to match the new values, bypassing this check.
The first SO question I linked suggests this is not an issue, because if the original values are forged in such a way, those properties won't be saved anyway:
For any other "unchanged" property, which we are not using in any way, we don't need to worry if it has been tampered with because, even if it has, the tampered value will not be persisted to the database
This is untrue however, as long as all modified properties on the entity have their original values forged. For example, the following code will bypass server-side security checks based on OriginalValuesMap and still save to the database:
manager.fetchEntityByKey('Employee', 42).then(function (result) {
var employee = result.entity;
employee.Salary(1000000); // do you think HR will notice?
delete employee.entityAspect.originalValues.Salary;
return manager.saveChanges();
});
When Breeze .NET receives the entity, it adds the entity to an Entity Framework context in Modified state, and with no properties marked as modified, Entity Framework's behaviour is to save all the supplied property values to the database.
IMO this is a security bug in EFContextProvider.HandleModified, where it overrides the EF entity state to Modified (there is even a comment in that method warning not to do so). In any case, what is the correct way to determine which properties have changed and are about to be saved?
In your Context intercept Save and check if it is legal save or not. For the sake of explanation, let's say you want to save entity of type RestrictedClass and you defined table RestrictedClasses which imitates table in your database.
public override int SaveChanges()
{
foreach (
var entry in
this.ChangeTracker.Entries()
.Where((e => (e.State == (EntityState) Breeze.WebApi.EntityState.Modified))))
{
if (entry.Entity.GetType() == typeof(RestrictedClass))
{
var entity = entry.Entity as RestrictedClass;
var originalEntities = RestrictedClasses.Where(e => e.Id = entity.Id).toList();
if (originalEntities.Count == 0) continue; // user is trying to add, illegal since it says it's modified, you do different check for EntityState.Added
var originalEntity = originalEntities[0]; // there should be only one, unique ID
//.... now you check differences between entity and originalEntity and decide whether it's legal or not based on user role.
Related
I have a function that get a Book entity, and checks whether it already exists in the database.
If it already exists, the function needs to update the entity in context.
So when I use the Find function to check whether it exists, the following error is thrown:
Attaching an entity of type 'Books' failed because another entity of the same type already has the same primary key value. This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate.
But when I use the Any function to check that, the code work fine.
My guess is that the Find function Attach the entity (to context) but Any not doing it.
Can someone give an explanation please?
The Find function:
public IHttpActionResult PutBook(Books book)
{
if(db.Books.Find(book.id) == null)
{
db.Entry(book).State = EntityState.Modified;
}
.
.
}
The Any function:
public IHttpActionResult PutBook(Books book)
{
if (db.Books.Any(b => b.id.Equals(book.id)))
{
db.Entry(book).State = EntityState.Modified;
}
.
.
}
Sorry if I have English errors.
Yes, Find fetches an entity from the database and attaches it to the context if it's not already in its cache. So at that point there is a Book instance in the context's cache and then you try to attach another instance to it. That's not allowed, because EF's cache is designed as an identity map.
The second snippet obviously doesn't materialize a Book instance: it runs a SQL query that only returns a bool.
We are parsing the SaveBundle on the server and returning a custom SaveResult. We want to be able to notify the client of additional changed entities as a result of processing the SaveBundle.
For example we have a SaveBundle from the client containing 1 entity to be deleted which when we parse and process on the server we actually delete 2 entities.
As far as we can tell the SaveResult does not contain any properties that would allow us to indicate an entity was 'deleted', rather than say 'modified'.
Is there a way to return additional entity changes through the SaveResult? Or is the only solution to refresh the data by resubmitting a Breeze query client side after the save changes?
I 'think' that if you return the deleted entities with their foreign keys set to null or empty (in the case of non-nullable guids etc.) in the SaveResult then Breeze client-side will detect this and mark them as deleted
I couldn't find anything explicitly in the documentation or the source about this though
here is your answer:
var result = context.SaveChanges(saveBundle);
//create your own EntityInfo object and fill it with the the entity and it's state
var entityInfo = new EntityInfo();
//...
//add it to the result
result.Entities.Add(entityInfo);
//return the result
return result;
Breeze client will then treat that entity like any other entity returned from you normal save proc.
Hope this helps
I extended my server entity with some properties in the client side .
When getting data from the query I really see these properties in the result filled with the proper values .
When I change a value of an extended properties the manager doesn't track this change .
When I call manager.rejectChanges() no action is happen , I debugged the code and I see in the entityAspect.entityState ("Unchaged") although I modified the property.
If I modify a property comes from the server entity every thing is ok.
Here is my Product entity in the server :
public class Product
{
public string Code {get;set;}
}
I extended the product in the client side with some others :
var Product = function () {
this.kind = ko.observable();
};
breeze.metadataStore.registerEntityTypeCtor("Product", Product);
After the query I get both field (Code , Kind) , if I change Code , entity state is modified , I can call manager.rejectChanges and its takes effect, but if I change kind nothing happen , the entity state is "Unchaged".
Any idea why this happen ?
Thanks in advance ...
By "extended" I assume you mean "unmapped" properties which are typically defined in a custom constructor as described in "Extending Entities"
"Unmapped" properties do not map to permanently stored values on the server. Therefore, changes to unmapped properties do not affect EntityState and they are not sent to the server.
Note that the server can supply the value of an unmapped property in the payload of a query and Breeze will set the unmapped property accordingly. This is a way to calculate non-persisted values on the server and transmit them to the entity on the client.
On the client an unmapped property behaves in other respects like a mapped property:
conforms to the syntax of the model library (e.g., it becomes an observable in KO models)
serialized when exported
validation rules apply
raises propertyChange when the value is changed
entity remembers the property's "original value"
rejectChanges() reverts the property to that original value
Don't know if this is intended or a bug, but the following code below using BeforeSaveEntity will only modify the entity for newly created records (EntityState = Added), and won't work for modified, is this correct?
protected override bool BeforeSaveEntity(EntityInfo entityInfo)
{
var entity = entityInfo.Entity;
if (entity is User)
{
var user = entity as User;
user.ModifiedDate = DateTime.Now;
user.ModifiedBy = 1;
}
...
The root of this issue is that on the breeze server we don’t have any built in change tracking mechanism for changes made on the server. Server entities can be pure poco. The breeze client has a rich change tracking capability for any client side changes but when you get to the server you need to manage this yourself.
The problem occurs because of an optimization we perform on the server so that we only update those properties that are changed. i.e. so that any SQL update statements are only made to the changed columns. Obviously this isn’t a problem for Adds or Deletes or those cases where we update a column that was already updated on the client. But if you update a field on the server that was not updated on the client then breeze doesn't know anything about it.
In theory we could snapshot each entity coming into the server and then iterate over every field on the entity to determine if any changes were made during save interception but we really hate the perf implications especially since this case will rarely occur.
So the suggestion made in another answer here to update the server side OriginalValuesMap is correct and will do exactly what you need.
In addition, as of version 1.1.3, there is an additional EntityInfo.ForceUpdate flag that you can set that will tell breeze to update every column in the specified entity. This isn't quite as performant as the suggestion above, but it is simpler, and the effects will be the same in either case.
Hope this helps.
I had the same problem, and I solved that doing this:
protected override bool BeforeSaveEntity(EntityInfo entityInfo)
{
if(entityInfo.EntityState== EntityState.Modified)
{
var entity = entityInfo.Entity;
entityInfo.OriginalValuesMap.Add("ModificationDate", entity.ModificationDate);
entity.ModificationDate = DateTime.Now;
}
}
I think you can apply this easily to your case.
I am looking for a simple way to hydrate a related object. A Note belongs to a Document and only owners of a Document can add Notes so when a user tries to edit a Note, I need to hydrate the related Document in order to find out if the user has access to it. In my Service layer I have the following:
public void editNote(Note note)
{
// Get the associated Document object (required for validation) and validate.
int docID = noteRepository.Find(note.NoteID).DocumentID;
note.Document = documentRepository.Find(docID);
IDictionary<string, string> errors = note.validate();
if (errors.Count > 0)
{
throw new ValidationException(errors);
}
// Update Repository and save.
noteRepository.InsertOrUpdate(note);
noteRepository.Save();
}
Trouble is, noteRepository.InsertOrUpdate(note) throws an exception with "An object with the same key already exists in the ObjectStateManager." when the repository sets EntityState.Modified. So a number of questions arise:
Am I approaching this correctly and if so, how do I get around the exception?
Currently, the controller edit action takes in a NoteCreateEditViewModel. Now this does have a DocumentID field as this is required when creating a new Note as we need to know which Document to attach it to. But for edit, I cannot use it as a malicious user could provide a DocumentID to which they do have access and thus edit a Note they don't own. So should there be seperate viewmodels for create and edit or can I just exclude the DocumentID somehow on edit? Or is there a better way to go about viewmodels such that an ID is not required?
Is there a better way to approach this? I have read that I should just have a Document repository as an aggregate and lose the Note repository but am not sure if/how this helps.
I asked a similar question related to this but it wasn't very clear so hoping this version will allow someone to understand and thus point me in the right direction.
EDIT
Based on the information provided by Ladislav Mrnka and the answer detailed here: An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key, it seems that my repository method need to be like the following:
public void InsertOrUpdate(Note note)
{
if (note.NoteID == default(int)) {
// New entity
context.Notes.Add(note);
} else {
// Existing entity
//context.Entry(note).State = EntityState.Modified;
context.Entry(oldNote).CurrentValues.SetValues(note);
}
}
But how do I get the oldNote from the context? I could call context.Entry(Find(note.NoteID)).CurrentValues.SetValues(note) but am I introducing potential problems here?
Am I approaching this correctly and if so, how do I get around the exception?
I guess this part of your code loads the whole Node from the database to find DocumentID:
int docID = noteRepository.Find(note.NoteID).DocumentID;
In such case your InsertOrUpdate cannot take your node and attach it to context with Modified state because you already have note with the same key in the context. Common solution is to use this:
objectContext.NoteSet.ApplyCurrentValues(note);
objectContext.SaveChanges();
But for edit, I cannot use it as a malicious user could provide a DocumentID to which they do have access and thus edit a Note they don't own.
In such case you must add some security. You can add any data into hidden fields in your page but those data which mustn't be changed by the client must contain some additional security. For example second hidden field with either signature computed on server or hash of salted value computed on server. When the data return in the next request to the server, it must recompute and compare signature / hash with same salt and validate that the passed value and computed value are same. Sure the client mustn't know the secret you are using to compute signature or salt used in hash.
I have read that I should just have a Document repository as an aggregate and lose the Note repository but am not sure if/how this helps.
This is cleaner way to use repositories but it will not help you with your particular error because you will still need Note and DocumentId.