diff --git a/docs/8.0/ayanova/docs/api-validation-error-codes.md b/docs/8.0/ayanova/docs/api-validation-error-codes.md index eea86890..ba05ed4c 100644 --- a/docs/8.0/ayanova/docs/api-validation-error-codes.md +++ b/docs/8.0/ayanova/docs/api-validation-error-codes.md @@ -12,3 +12,4 @@ In each case there may be more details in the `message` property where appropria | InvalidValue | Generic error indicating an input object's property is not set correctly | | ReferentialIntegrity | Indicates modifying the object (usually a delete) will break the link to other records in the database. The other records need to be modified before continuing | | InvalidOperation | Indicates the operation is invalid, details provided in the `message` | +| NotChangeable | Indicates the attempted property change is invalid because the value is fixed and cannot be changed | diff --git a/server/AyaNova/biz/UserBiz.cs b/server/AyaNova/biz/UserBiz.cs index 3bed56eb..03cdb277 100644 --- a/server/AyaNova/biz/UserBiz.cs +++ b/server/AyaNova/biz/UserBiz.cs @@ -203,6 +203,33 @@ namespace AyaNova.Biz //patch internal bool Patch(User dbObj, JsonPatchDocument objectPatch, uint concurrencyToken) { + //check for in-valid patches + if (objectPatch.Operations.Any(m => m.path == "/id")) + { + AddError(ValidationErrorType.NotChangeable, "Id"); + return false; + } + + if (objectPatch.Operations.Any(m => m.path == "/ownerid")) + { + AddError(ValidationErrorType.NotChangeable, "OwnerId"); + return false; + } + + if (objectPatch.Operations.Any(m => m.op == "add")) + { + AddError(ValidationErrorType.InvalidOperation, "add"); + return false; + } + + if (objectPatch.Operations.Any(m => m.op == "remove")) + { + AddError(ValidationErrorType.InvalidOperation, "remove"); + return false; + } + + + //make a snapshot of the original for validation but update the original to preserve workflow User snapshotObj = new User(); CopyObject.Copy(dbObj, snapshotObj); diff --git a/server/AyaNova/biz/UserOptionsBiz.cs b/server/AyaNova/biz/UserOptionsBiz.cs new file mode 100644 index 00000000..8b2e00fb --- /dev/null +++ b/server/AyaNova/biz/UserOptionsBiz.cs @@ -0,0 +1,120 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.JsonPatch; +using EnumsNET; +using AyaNova.Util; +using AyaNova.Api.ControllerHelpers; +using AyaNova.Biz; +using AyaNova.Models; + + +namespace AyaNova.Biz +{ + + + internal class UserOptionsBiz : BizObject + { + private readonly AyContext ct; + public readonly long userId; + private readonly AuthorizationRoles userRoles; + + + internal UserOptionsBiz(AyContext dbcontext, long currentUserId, AuthorizationRoles UserRoles) + { + ct = dbcontext; + userId = currentUserId; + userRoles = UserRoles; + } + + + //////////////////////////////////////////////////////////////////////////////////////////////// + /// GET + + //Get one + internal async Task GetAsync(long fetchId) + { + //This is simple so nothing more here, but often will be copying to a different output object or some other ops + return await ct.UserOptions.SingleOrDefaultAsync(m => m.Id == fetchId); + } + + + //////////////////////////////////////////////////////////////////////////////////////////////// + //UPDATE + // + + //put + internal bool Put(UserOptions dbObj, UserOptions inObj) + { + + //Replace the db object with the PUT object + CopyObject.Copy(inObj, dbObj, "Id"); + //Set "original" value of concurrency token to input token + //this will allow EF to check it out + ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = inObj.ConcurrencyToken; + + Validate(dbObj); + if (HasErrors) + return false; + + return true; + } + + //patch + internal bool Patch(UserOptions dbObj, JsonPatchDocument objectPatch, uint concurrencyToken) + { + //check for in-valid patches + if(objectPatch.Operations.Any(m=>m.path=="Id")) + { + AddError(ValidationErrorType.InvalidOperation,"Id"); + return false; + } + + //Do the patching + objectPatch.ApplyTo(dbObj); + + + ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = concurrencyToken; + Validate(dbObj); + if (HasErrors) + return false; + + return true; + } + + + + + //////////////////////////////////////////////////////////////////////////////////////////////// + //VALIDATION + // + + //Can save or update? + private void Validate(UserOptions inObj) + { + //UserOptions is never new, it's created with the User object so were only here for an edit + + + //OwnerId required + if (inObj.OwnerId == 0) + AddError(ValidationErrorType.RequiredPropertyEmpty, "OwnerId"); + + //OwnerId required + if (inObj.UserId == 0) + AddError(ValidationErrorType.RequiredPropertyEmpty, "UserId"); + + //LOOKAT:Validate email address is legitimate (I put the EMailAddress attribute on the field in the model so I think it might validate) + + return; + } + + + + ///////////////////////////////////////////////////////////////////// + + }//eoc + + +}//eons + diff --git a/server/AyaNova/biz/ValidationErrorType.cs b/server/AyaNova/biz/ValidationErrorType.cs index ce809247..26d8b5f4 100644 --- a/server/AyaNova/biz/ValidationErrorType.cs +++ b/server/AyaNova/biz/ValidationErrorType.cs @@ -9,7 +9,8 @@ namespace AyaNova.Biz StartDateMustComeBeforeEndDate = 4, InvalidValue = 5, ReferentialIntegrity = 6, - InvalidOperation = 7 + InvalidOperation = 7, + NotChangeable=8 //!! NOTE - UPDATE api-validation-error-codes.md documentation when adding items diff --git a/server/AyaNova/models/AyContext.cs b/server/AyaNova/models/AyContext.cs index 297c2612..554020ab 100644 --- a/server/AyaNova/models/AyContext.cs +++ b/server/AyaNova/models/AyContext.cs @@ -82,7 +82,8 @@ namespace AyaNova.Models modelBuilder.Entity() .HasOne(p => p.UserOptions) .WithOne(i => i.User) - .HasForeignKey(b => b.UserId); + .HasForeignKey(b => b.UserId) + .OnDelete(DeleteBehavior.Cascade);//Hopefully will delete the useroptions with the user? //----------- diff --git a/server/AyaNova/models/UserOptions.cs b/server/AyaNova/models/UserOptions.cs index 9b0d3792..81963bb8 100644 --- a/server/AyaNova/models/UserOptions.cs +++ b/server/AyaNova/models/UserOptions.cs @@ -14,6 +14,7 @@ namespace AyaNova.Models public long OwnerId { get; set; } //------------- + [EmailAddress] public string EmailAddress { get; set; } public decimal TimeZoneOffset { get; set; } public int UiColor { get; set; } diff --git a/test/raven-integration/User/UserCrud.cs b/test/raven-integration/User/UserCrud.cs index e35704a0..c60d5b52 100644 --- a/test/raven-integration/User/UserCrud.cs +++ b/test/raven-integration/User/UserCrud.cs @@ -87,7 +87,7 @@ namespace raven_integration ApiResponse DELETETestResponse = await Util.DeleteAsync("User/" + d2Id.ToString(), await Util.GetTokenAsync("manager", "l3tm3in")); Util.ValidateHTTPStatusCode(DELETETestResponse, 204); } - + /// /// Test not found @@ -178,6 +178,52 @@ namespace raven_integration } + + /// + /// + /// + [Fact] + public async void DisallowedPatchAttemptsShouldFail() + { + //CREATE + dynamic D = new JObject(); + D.name = Util.Uniquify("DisallowedPatchAttemptsShouldFail"); + D.ownerId = 1L; + D.active = true; + D.login = Util.Uniquify("LOGIN"); + D.password = Util.Uniquify("PASSWORD"); + D.roles = 0;//norole + D.localeId = 1;//random locale + D.userType = 3;//non scheduleable + + ApiResponse R = await Util.PostAsync("User", await Util.GetTokenAsync("manager", "l3tm3in"), D.ToString()); + Util.ValidateDataReturnResponseOk(R); + long w2Id = R.ObjectResponse["result"]["id"].Value(); + uint OriginalConcurrencyToken = R.ObjectResponse["result"]["concurrencyToken"].Value(); + + + //PATCH attempt on Id + string patchJson = "[{\"value\": \"0\",\"path\": \"/id\",\"op\": \"replace\"}]"; + ApiResponse PATCHTestResponse = await Util.PatchAsync("User/" + w2Id.ToString() + "/" + (OriginalConcurrencyToken - 1).ToString(), await Util.GetTokenAsync("manager", "l3tm3in"), patchJson); + Util.ValidateErrorCodeResponse(PATCHTestResponse, 2200, 400); + + //PATCH attempt on OwnerId + patchJson = "[{\"value\": \"0\",\"path\": \"/ownerid\",\"op\": \"replace\"}]"; + PATCHTestResponse = await Util.PatchAsync("User/" + w2Id.ToString() + "/" + (OriginalConcurrencyToken - 1).ToString(), await Util.GetTokenAsync("manager", "l3tm3in"), patchJson); + Util.ValidateErrorCodeResponse(PATCHTestResponse, 2200, 400); + + //PATCH attempt add field + patchJson = "[{\"value\": \"0\",\"path\": \"/bogus\",\"op\": \"add\"}]"; + PATCHTestResponse = await Util.PatchAsync("User/" + w2Id.ToString() + "/" + (OriginalConcurrencyToken - 1).ToString(), await Util.GetTokenAsync("manager", "l3tm3in"), patchJson); + Util.ValidateErrorCodeResponse(PATCHTestResponse, 2200, 400); + + //PATCH attempt remove name field + patchJson = "[{\"path\": \"/name\",\"op\": \"remove\"}]"; + PATCHTestResponse = await Util.PatchAsync("User/" + w2Id.ToString() + "/" + (OriginalConcurrencyToken - 1).ToString(), await Util.GetTokenAsync("manager", "l3tm3in"), patchJson); + Util.ValidateErrorCodeResponse(PATCHTestResponse, 2200, 400); + } + + /// /// /// @@ -210,7 +256,7 @@ namespace raven_integration //PUT var NewPassword = "NEW_PASSWORD"; D.password = NewPassword; - D.concurrencyToken=OriginalConcurrencyToken; + D.concurrencyToken = OriginalConcurrencyToken; R = await Util.PutAsync("User/" + UserId.ToString(), await Util.GetTokenAsync("manager", "l3tm3in"), D.ToString()); Util.ValidateDataReturnResponseOk(R);