From add560830a19535b0aec69d133dbea955c2cc9d5 Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Thu, 6 Sep 2018 18:10:13 +0000 Subject: [PATCH] --- .../Controllers/UserOptionsController.cs | 308 ++---------------- server/AyaNova/biz/AyaType.cs | 3 +- server/AyaNova/biz/BizRoles.cs | 15 +- server/AyaNova/biz/UserOptionsBiz.cs | 7 +- test/raven-integration/User/UserOptionsRu.cs | 131 ++++++++ 5 files changed, 178 insertions(+), 286 deletions(-) create mode 100644 test/raven-integration/User/UserOptionsRu.cs diff --git a/server/AyaNova/Controllers/UserOptionsController.cs b/server/AyaNova/Controllers/UserOptionsController.cs index 8b879bfa..9a75194f 100644 --- a/server/AyaNova/Controllers/UserOptionsController.cs +++ b/server/AyaNova/Controllers/UserOptionsController.cs @@ -15,7 +15,7 @@ using AyaNova.Biz; namespace AyaNova.Api.Controllers { - + /// /// UserOptions /// @@ -44,15 +44,13 @@ namespace AyaNova.Api.Controllers } - - /// /// Get full UserOptions object /// /// Required roles: - /// BizAdminFull, InventoryFull, BizAdminLimited, InventoryLimited, TechFull, TechLimited, Accounting + /// BizAdminFull, BizAdminLimited or be users own record /// - /// + /// UserId /// A single UserOptions [HttpGet("{id}")] public async Task GetUserOptions([FromRoute] long id) @@ -62,7 +60,10 @@ namespace AyaNova.Api.Controllers return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason)); } - if (!Authorized.IsAuthorizedToReadFullRecord(HttpContext.Items, AyaType.UserOptions)) + var UserId = UserIdFromContext.Id(HttpContext.Items); + + //Different than normal here: a user is *always* allowed to retrieve their own user options object + if (id != UserId && !Authorized.IsAuthorizedToReadFullRecord(HttpContext.Items, AyaType.UserOptions)) { return StatusCode(401, new ApiNotAuthorizedResponse()); } @@ -73,101 +74,29 @@ namespace AyaNova.Api.Controllers } //Instantiate the business object handler - UserOptionsBiz biz = new UserOptionsBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items)); + UserOptionsBiz biz = new UserOptionsBiz(ct, UserId, UserRolesFromContext.Roles(HttpContext.Items)); var o = await biz.GetAsync(id); if (o == null) { return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); - } - - //Log - EventLogProcessor.AddEntry(new Event(biz.userId, o.Id, AyaType.UserOptions, AyaEvent.Retrieved), ct); - ct.SaveChanges(); + } + return Ok(new ApiOkResponse(o)); } - /// - /// Get paged list of UserOptionss - /// - /// Required roles: Any - /// - /// - /// Paged collection of UserOptionss with paging data - [HttpGet("ListUserOptionss", Name = nameof(ListUserOptionss))]//We MUST have a "Name" defined or we can't get the link for the pagination, non paged urls don't need a name - public async Task ListUserOptionss([FromQuery] PagingOptions pagingOptions) - { - if (serverState.IsClosed) - { - return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason)); - } - - if (!Authorized.IsAuthorizedToReadFullRecord(HttpContext.Items, AyaType.UserOptions)) - { - return StatusCode(401, new ApiNotAuthorizedResponse()); - } - - if (!ModelState.IsValid) - { - return BadRequest(new ApiErrorResponse(ModelState)); - } - - //Instantiate the business object handler - UserOptionsBiz biz = new UserOptionsBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items)); - - ApiPagedResponse pr = await biz.GetManyAsync(Url, nameof(ListUserOptionss), pagingOptions); - return Ok(new ApiOkWithPagingResponse(pr)); - } - - - - /// - /// Get UserOptions pick list - /// - /// Required roles: Any - /// - /// This list supports querying the Name property - /// include a "q" parameter for string to search for - /// use % for wildcards. - /// - /// e.g. q=%Jones% - /// - /// Query is case insensitive - /// - /// Paged id/name collection of UserOptionss with paging data - [HttpGet("PickList", Name = nameof(UserOptionsPickList))] - public async Task UserOptionsPickList([FromQuery] string q, [FromQuery] PagingOptions pagingOptions) - { - if (serverState.IsClosed) - { - return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason)); - } - - if (!ModelState.IsValid) - { - return BadRequest(new ApiErrorResponse(ModelState)); - } - - //Instantiate the business object handler - UserOptionsBiz biz = new UserOptionsBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items)); - - ApiPagedResponse pr = await biz.GetPickListAsync(Url, nameof(UserOptionsPickList), pagingOptions, q); - return Ok(new ApiOkWithPagingResponse(pr)); - } - - + /// /// Put (update) UserOptions /// /// Required roles: - /// BizAdminFull, InventoryFull - /// TechFull (owned only) + /// BizAdminFull or be users own record /// /// - /// + /// User id /// /// [HttpPut("{id}")] @@ -182,15 +111,16 @@ namespace AyaNova.Api.Controllers { return BadRequest(new ApiErrorResponse(ModelState)); } + var UserId = UserIdFromContext.Id(HttpContext.Items); - var o = await ct.UserOptions.SingleOrDefaultAsync(m => m.Id == id); + var o = await ct.UserOptions.SingleOrDefaultAsync(m => m.UserId == id); if (o == null) { return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); } - if (!Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.UserOptions, o.OwnerId)) + if (id != UserId && !Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.UserOptions, o.OwnerId)) { return StatusCode(401, new ApiNotAuthorizedResponse()); } @@ -217,15 +147,11 @@ namespace AyaNova.Api.Controllers } else { - //exists but was changed by another user - //I considered returning new and old record, but where would it end? - //Better to let the client decide what to do than to send extra data that is not required + //exists but was changed by another user return StatusCode(409, new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT)); } } - - return Ok(new ApiOkResponse(new { ConcurrencyToken = o.ConcurrencyToken })); } @@ -235,17 +161,15 @@ namespace AyaNova.Api.Controllers /// Patch (update) UserOptions /// /// Required roles: - /// BizAdminFull, InventoryFull - /// TechFull (owned only) + /// BizAdminFull or be users own record /// - /// + /// UserId /// /// /// [HttpPatch("{id}/{concurrencyToken}")] public async Task PatchUserOptions([FromRoute] long id, [FromRoute] uint concurrencyToken, [FromBody]JsonPatchDocument objectPatch) - { - //https://dotnetcoretutorials.com/2017/11/29/json-patch-asp-net-core/ + { if (!serverState.IsOpen) { @@ -257,18 +181,19 @@ namespace AyaNova.Api.Controllers return BadRequest(new ApiErrorResponse(ModelState)); } + var UserId = UserIdFromContext.Id(HttpContext.Items); + //Instantiate the business object handler - UserOptionsBiz biz = new UserOptionsBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items)); + UserOptionsBiz biz = new UserOptionsBiz(ct, UserId, UserRolesFromContext.Roles(HttpContext.Items)); - - var o = await ct.UserOptions.SingleOrDefaultAsync(m => m.Id == id); + var o = await ct.UserOptions.SingleOrDefaultAsync(m => m.UserId == id); if (o == null) { return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); } - if (!Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.UserOptions, o.OwnerId)) + if (id != UserId && !Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.UserOptions, o.OwnerId)) { return StatusCode(401, new ApiNotAuthorizedResponse()); } @@ -297,197 +222,18 @@ namespace AyaNova.Api.Controllers } } - - - return Ok(new ApiOkResponse(new { ConcurrencyToken = o.ConcurrencyToken })); } - /// - /// Post UserOptions - /// - /// Required roles: - /// BizAdminFull, InventoryFull, TechFull - /// - /// - /// - [HttpPost] - public async Task PostUserOptions([FromBody] UserOptions inObj) - { - if (!serverState.IsOpen) - { - return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason)); - } - - //If a user has change roles, or editOwnRoles then they can create, true is passed for isOwner since they are creating so by definition the owner - if (!Authorized.IsAuthorizedToCreate(HttpContext.Items, AyaType.UserOptions)) - { - return StatusCode(401, new ApiNotAuthorizedResponse()); - } - - if (!ModelState.IsValid) - { - return BadRequest(new ApiErrorResponse(ModelState)); - } - - //Instantiate the business object handler - UserOptionsBiz biz = new UserOptionsBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items)); - - //Create and validate - UserOptions o = await biz.CreateAsync(inObj); - - if (o == null) - { - //error return - return BadRequest(new ApiErrorResponse(biz.Errors)); - } - else - { - - //save to get Id - await ct.SaveChangesAsync(); - - //Log now that we have the Id - EventLogProcessor.AddEntry(new Event(biz.userId, o.Id, AyaType.UserOptions, AyaEvent.Created), ct); - await ct.SaveChangesAsync(); - - //return success and link - return CreatedAtAction("GetUserOptions", new { id = o.Id }, new ApiCreatedResponse(o)); - } - } - - - - /// - /// Delete UserOptions - /// - /// Required roles: - /// BizAdminFull, InventoryFull - /// TechFull (owned only) - /// - /// - /// - /// Ok - [HttpDelete("{id}")] - public async Task DeleteUserOptions([FromRoute] long id) - { - - if (!serverState.IsOpen) - { - return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason)); - } - - if (!ModelState.IsValid) - { - return BadRequest(new ApiErrorResponse(ModelState)); - } - - var dbObj = await ct.UserOptions.SingleOrDefaultAsync(m => m.Id == id); - if (dbObj == null) - { - return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); - } - - if (!Authorized.IsAuthorizedToDelete(HttpContext.Items, AyaType.UserOptions, dbObj.OwnerId)) - { - return StatusCode(401, new ApiNotAuthorizedResponse()); - } - - //Instantiate the business object handler - UserOptionsBiz biz = new UserOptionsBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items)); - if (!biz.Delete(dbObj)) - { - return BadRequest(new ApiErrorResponse(biz.Errors)); - } - - //Log - EventLogProcessor.DeleteObject(biz.userId, AyaType.UserOptions, dbObj.Id, dbObj.Name, ct); - - await ct.SaveChangesAsync(); - - //Delete children / attached objects - biz.DeleteChildren(dbObj); - - - return NoContent(); - } - - - private bool UserOptionsExists(long id) { - return ct.UserOptions.Any(e => e.Id == id); + //NOTE: checks by UserId, NOT by Id as in most other objects + return ct.UserOptions.Any(e => e.UserId == id); } - /// - /// Get route that triggers exception for testing - /// - /// Nothing, triggers exception - [HttpGet("exception")] - public ActionResult GetException() - { - if (!serverState.IsOpen) - { - return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason)); - } - - if (!Authorized.IsAuthorizedToReadFullRecord(HttpContext.Items, AyaType.UserOptions)) - { - return StatusCode(401, new ApiNotAuthorizedResponse()); - } - - throw new System.NotSupportedException("Test exception from UserOptions controller"); - } - - /// - /// Get route that triggers an alternate type of exception for testing - /// - /// Nothing, triggers exception - [HttpGet("altexception")] - public ActionResult GetAltException() - { - if (!serverState.IsOpen) - { - return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason)); - } - - if (!Authorized.IsAuthorizedToReadFullRecord(HttpContext.Items, AyaType.UserOptions)) - { - return StatusCode(401, new ApiNotAuthorizedResponse()); - } - - throw new System.ArgumentException("Test exception (ALT) from UserOptions controller"); - } - - - /// - /// Get route that submits a simulated long running operation job for testing - /// - /// Nothing - [HttpGet("TestUserOptionsJob")] - public ActionResult TestUserOptionsJob() - { - if (!serverState.IsOpen) - { - return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason)); - } - - if (!Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.JobOperations)) - { - return StatusCode(401, new ApiNotAuthorizedResponse()); - } - - //Create the job here - OpsJob j = new OpsJob(); - j.Name = "TestUserOptionsJob"; - j.JobType = JobType.TestUserOptionsJob; - JobsBiz.AddJob(j, ct); - return Accepted(new { JobId = j.GId });//202 accepted - } - //------------ diff --git a/server/AyaNova/biz/AyaType.cs b/server/AyaNova/biz/AyaType.cs index 5bebd5e3..722b37d0 100644 --- a/server/AyaNova/biz/AyaType.cs +++ b/server/AyaNova/biz/AyaType.cs @@ -25,7 +25,8 @@ namespace AyaNova.Biz AyaNova7Import = 10, TrialSeeder = 11, Metrics = 12, - Locale = 13 + Locale = 13, + UserOptions=14 } diff --git a/server/AyaNova/biz/BizRoles.cs b/server/AyaNova/biz/BizRoles.cs index 704e079f..56b833a9 100644 --- a/server/AyaNova/biz/BizRoles.cs +++ b/server/AyaNova/biz/BizRoles.cs @@ -23,7 +23,7 @@ namespace AyaNova.Biz //CHANGE = CREATE, RETRIEVE, UPDATE, DELETE - Full rights //EDITOWN = special subset of CHANGE: You can create and if it's one you created then you have rights to edit it or delete, but you can't edit ones others have created //READ = You can read *all* the fields of the record, but can't modify it. Change is automatically checked for so only add different roles from change - //PICKLIST NOTE: this does not control getting a list of names for selection which is role independent because it's required for so much indirectly + //PICKLIST NOTE: this does not control getting a list of names for selection which is role independent because it's required for so much indirectly //DELETE = There is no specific delete right for now though it's checked for by routes in Authorized.cs in case we want to add it in future as a separate right from create. #region All roles initialization @@ -38,6 +38,19 @@ namespace AyaNova.Biz ReadFullRecord = AuthorizationRoles.BizAdminLimited }); + //////////////////////////////////////////////////////////// + //USEROPTIONS + //(Identical to User, though route also allows own record access full changes) + // + roles.Add(AyaType.UserOptions, new BizRoleSet() + { + Change = AuthorizationRoles.BizAdminFull, + EditOwn = AuthorizationRoles.NoRole,//no one can make a user but a bizadminfull + ReadFullRecord = AuthorizationRoles.BizAdminLimited + }); + + + //////////////////////////////////////////////////////////// //WIDGET // diff --git a/server/AyaNova/biz/UserOptionsBiz.cs b/server/AyaNova/biz/UserOptionsBiz.cs index f909eca8..d0d46883 100644 --- a/server/AyaNova/biz/UserOptionsBiz.cs +++ b/server/AyaNova/biz/UserOptionsBiz.cs @@ -35,8 +35,9 @@ namespace AyaNova.Biz //Get one internal async Task GetAsync(long fetchId) { + //NOTE: get by UserId as there is a 1:1 relationship, not by useroptions id //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); + return await ct.UserOptions.SingleOrDefaultAsync(m => m.UserId == fetchId); } @@ -49,7 +50,7 @@ namespace AyaNova.Biz { //Replace the db object with the PUT object - CopyObject.Copy(inObj, dbObj, "Id"); + CopyObject.Copy(inObj, dbObj, "Id, UserId, OwnerId"); //Set "original" value of concurrency token to input token //this will allow EF to check it out ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = inObj.ConcurrencyToken; @@ -65,7 +66,7 @@ namespace AyaNova.Biz internal bool Patch(UserOptions dbObj, JsonPatchDocument objectPatch, uint concurrencyToken) { //Validate Patch is allowed - if (!ValidateJsonPatch.Validate(this, objectPatch)) return false; + if (!ValidateJsonPatch.Validate(this, objectPatch, "UserId")) return false; //Do the patching objectPatch.ApplyTo(dbObj); diff --git a/test/raven-integration/User/UserOptionsRu.cs b/test/raven-integration/User/UserOptionsRu.cs new file mode 100644 index 00000000..aa4e5a33 --- /dev/null +++ b/test/raven-integration/User/UserOptionsRu.cs @@ -0,0 +1,131 @@ +using System; +using Xunit; +using Newtonsoft.Json.Linq; +using FluentAssertions; + +namespace raven_integration +{ + + public class UserOptionsRu + { + + /// + /// Test all CRUD routes for a UserOptions object + /// + [Fact] + public async void RU() + { + + //CREATE a user + dynamic D1 = new JObject(); + D1.name = Util.Uniquify("Test UserOptions User"); + D1.ownerId = 1L; + D1.active = true; + D1.login = Util.Uniquify("LOGIN"); + D1.password = Util.Uniquify("PASSWORD"); + D1.roles = 0;//norole + D1.localeId = 1;//random locale + D1.userType = 3;//non scheduleable + + ApiResponse R = await Util.PostAsync("User", await Util.GetTokenAsync("manager", "l3tm3in"), D1.ToString()); + Util.ValidateDataReturnResponseOk(R); + long UserId = R.ObjectResponse["result"]["id"].Value(); + + //Now there should be a user options available for this user + + + //RETRIEVE companion USEROPTIONS object + + //Get it + R = await Util.GetAsync("UserOptions/" + UserId.ToString(), await Util.GetTokenAsync("manager", "l3tm3in")); + Util.ValidateDataReturnResponseOk(R); + //ensure the default value is set + R.ObjectResponse["result"]["uiColor"].Value().Should().Be(0); + uint concurrencyToken = R.ObjectResponse["result"]["concurrencyToken"].Value(); + + //UPDATE + //PUT + dynamic D2 = new JObject(); + D2.emailaddress = "testuseroptions@helloayanova.com"; + D2.TimeZoneOffset = -7.5M;//Decimal value + D2.UiColor = -2097216;//Int value (no suffix for int literals) + D2.concurrencyToken = concurrencyToken; + ApiResponse PUTTestResponse = await Util.PutAsync("UserOptions/" + UserId.ToString(), await Util.GetTokenAsync("manager", "l3tm3in"), D2.ToString()); + Util.ValidateHTTPStatusCode(PUTTestResponse, 200); + + //VALIDATE + R = await Util.GetAsync("UserOptions/" + UserId.ToString(), await Util.GetTokenAsync("manager", "l3tm3in")); + Util.ValidateDataReturnResponseOk(R); + //ensure the default value is set + R.ObjectResponse["result"]["emailAddress"].Value().Should().Be(D2.emailaddress.ToString()); + R.ObjectResponse["result"]["timeZoneOffset"].Value().Should().Be((decimal)D2.TimeZoneOffset); + R.ObjectResponse["result"]["uiColor"].Value().Should().Be((int)D2.UiColor); + concurrencyToken = R.ObjectResponse["result"]["concurrencyToken"].Value(); + + // //update w2id + // D2.name = Util.Uniquify("UPDATED VIA PUT SECOND TEST User"); + // D2.OwnerId = 1; + // D2.concurrencyToken = R2.ObjectResponse["result"]["concurrencyToken"].Value(); + // ApiResponse PUTTestResponse = await Util.PutAsync("User/" + d2Id.ToString(), await Util.GetTokenAsync("manager", "l3tm3in"), D2.ToString()); + // Util.ValidateHTTPStatusCode(PUTTestResponse, 200); + + // //check PUT worked + // ApiResponse checkPUTWorked = await Util.GetAsync("User/" + d2Id.ToString(), await Util.GetTokenAsync("manager", "l3tm3in")); + // Util.ValidateNoErrorInResponse(checkPUTWorked); + // checkPUTWorked.ObjectResponse["result"]["name"].Value().Should().Be(D2.name.ToString()); + // uint concurrencyToken = PUTTestResponse.ObjectResponse["result"]["concurrencyToken"].Value(); + + // //PATCH + // var newName = Util.Uniquify("UPDATED VIA PATCH SECOND TEST User"); + // string patchJson = "[{\"value\": \"" + newName + "\",\"path\": \"/name\",\"op\": \"replace\"}]"; + // ApiResponse PATCHTestResponse = await Util.PatchAsync("User/" + d2Id.ToString() + "/" + concurrencyToken.ToString(), await Util.GetTokenAsync("manager", "l3tm3in"), patchJson); + // Util.ValidateHTTPStatusCode(PATCHTestResponse, 200); + + // //check PATCH worked + // ApiResponse checkPATCHWorked = await Util.GetAsync("User/" + d2Id.ToString(), await Util.GetTokenAsync("manager", "l3tm3in")); + // Util.ValidateNoErrorInResponse(checkPATCHWorked); + // checkPATCHWorked.ObjectResponse["result"]["name"].Value().Should().Be(newName); + + // //DELETE + // ApiResponse DELETETestResponse = await Util.DeleteAsync("User/" + d2Id.ToString(), await Util.GetTokenAsync("manager", "l3tm3in")); + // Util.ValidateHTTPStatusCode(DELETETestResponse, 204); + } + + + /// + /// Test not found + /// + [Fact] + public async void GetNonExistentItemShouldError() + { + //Get non existant + //Should return status code 404, api error code 2010 + ApiResponse R = await Util.GetAsync("UserOptions/999999", await Util.GetTokenAsync("manager", "l3tm3in")); + Util.ValidateResponseNotFound(R); + } + + /// + /// Test bad modelstate + /// + [Fact] + public async void GetBadModelStateShouldError() + { + //Get non existant + //Should return status code 400, api error code 2200 and a first target in details of "id" + ApiResponse R = await Util.GetAsync("UserOptions/2q2", await Util.GetTokenAsync("manager", "l3tm3in")); + Util.ValidateBadModelStateResponse(R, "id"); + } + + + + + + + + + + + //================================================== + + }//eoc +}//eons