From ba7344df695d85b7a2d08f02eec0edc8fc2281da Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Wed, 15 Jun 2022 17:46:22 +0000 Subject: [PATCH] --- .../Controllers/IntegrationController.cs | 136 ++++++++++++ server/AyaNova/biz/BizRoles.cs | 59 +++++- server/AyaNova/biz/IntegrationBiz.cs | 193 ++++++++++++++++++ server/AyaNova/models/Integration.cs | 2 +- 4 files changed, 385 insertions(+), 5 deletions(-) create mode 100644 server/AyaNova/Controllers/IntegrationController.cs create mode 100644 server/AyaNova/biz/IntegrationBiz.cs diff --git a/server/AyaNova/Controllers/IntegrationController.cs b/server/AyaNova/Controllers/IntegrationController.cs new file mode 100644 index 00000000..dac5b6ea --- /dev/null +++ b/server/AyaNova/Controllers/IntegrationController.cs @@ -0,0 +1,136 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Logging; +using AyaNova.Models; +using AyaNova.Api.ControllerHelpers; +using AyaNova.Biz; + + +namespace AyaNova.Api.Controllers +{ + [ApiController] + [ApiVersion("8.0")] + [Route("api/v{version:apiVersion}/integration")] + [Produces("application/json")] + [Authorize] + public class IntegrationController : ControllerBase + { + private readonly AyContext ct; + private readonly ILogger log; + private readonly ApiServerState serverState; + + /// + /// ctor + /// + /// + /// + /// + public IntegrationController(AyContext dbcontext, ILogger logger, ApiServerState apiServerState) + { + ct = dbcontext; + log = logger; + serverState = apiServerState; + } + + /// + /// Create Integration + /// + /// + /// From route path + /// + [HttpPost] + public async Task PostIntegration([FromBody] Integration newObject, ApiVersion apiVersion) + { + if (!serverState.IsOpen) + return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); + IntegrationBiz biz = IntegrationBiz.GetBiz(ct, HttpContext); + if (!Authorized.HasCreateRole(HttpContext.Items, biz.BizType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + Integration o = await biz.CreateAsync(newObject); + if (o == null) + return BadRequest(new ApiErrorResponse(biz.Errors)); + else + return CreatedAtAction(nameof(IntegrationController.GetIntegration), new { id = o.Id, version = apiVersion.ToString() }, new ApiCreatedResponse(o)); + } + + + /// + /// Get Integration + /// + /// + /// Integration + [HttpGet("{id}")] + public async Task GetIntegration([FromRoute] long id) + { + if (!serverState.IsOpen) + return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); + IntegrationBiz biz = IntegrationBiz.GetBiz(ct, HttpContext); + if (!Authorized.HasReadFullRole(HttpContext.Items, biz.BizType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + var o = await biz.GetAsync(id, true, true); + if (o == null) return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); + return Ok(ApiOkResponse.Response(o)); + } + + /// + /// Update Integration + /// + /// + /// + [HttpPut] + public async Task PutIntegration([FromBody] Integration updatedObject) + { + if (!serverState.IsOpen) + return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + IntegrationBiz biz = IntegrationBiz.GetBiz(ct, HttpContext); + if (!Authorized.HasModifyRole(HttpContext.Items, biz.BizType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + var o = await biz.PutAsync(updatedObject); + if (o == null) + { + if (biz.Errors.Exists(z => z.Code == ApiErrorCode.CONCURRENCY_CONFLICT)) + return StatusCode(409, new ApiErrorResponse(biz.Errors)); + else + return BadRequest(new ApiErrorResponse(biz.Errors)); + } + return Ok(ApiOkResponse.Response(new { Concurrency = o.Concurrency })); ; + } + + /// + /// Delete Integration + /// + /// + /// NoContent + [HttpDelete("{id}")] + public async Task DeleteIntegration([FromRoute] long id) + { + if (!serverState.IsOpen) + return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + IntegrationBiz biz = IntegrationBiz.GetBiz(ct, HttpContext); + if (!Authorized.HasDeleteRole(HttpContext.Items, biz.BizType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + if (!await biz.DeleteAsync(id)) + return BadRequest(new ApiErrorResponse(biz.Errors)); + return NoContent(); + } + + + + + + //------------ + + + }//eoc +}//eons \ No newline at end of file diff --git a/server/AyaNova/biz/BizRoles.cs b/server/AyaNova/biz/BizRoles.cs index 06fd3e25..91f1addd 100644 --- a/server/AyaNova/biz/BizRoles.cs +++ b/server/AyaNova/biz/BizRoles.cs @@ -1043,14 +1043,65 @@ namespace AyaNova.Biz }); //////////////////////////////////////////////////////////// - //INTEGRATION - note this is for the management UI - //for *all* integrations. Separately, each integration app - //will have it's own authorization system + //INTEGRATION + // (everyone but outside users Customer and HO) + //this right is for the integration data itself, NOT any other AyaNova data + //so if someone is malicious the worst case scenario is they can mess up the integration data + // but they would still need rights to access any AyaNova data under their account so there is no loophole here + // technically an integration may be used by any role user + // however not likely to be read only or limited rights rols + // so will allow full access for any user and leave + // finer tuning of authorization to integrating app itself + // Also, integration is only used to store app data conveniently it in no way is required to + // write api accessing apps so any limitations are not preventing 3rd parties from writing AyaNova api consuming apps of any kind // roles.Add(AyaType.Integration, new BizRoleSet() { - Change = AuthorizationRoles.BizAdmin, + Change = AuthorizationRoles.BizAdminRestricted + | AuthorizationRoles.BizAdmin + | AuthorizationRoles.ServiceRestricted + | AuthorizationRoles.Service + | AuthorizationRoles.InventoryRestricted + | AuthorizationRoles.Inventory + | AuthorizationRoles.Accounting + | AuthorizationRoles.TechRestricted + | AuthorizationRoles.Tech + | AuthorizationRoles.SubContractorRestricted + | AuthorizationRoles.SubContractor + | AuthorizationRoles.Sales + | AuthorizationRoles.SalesRestricted + | AuthorizationRoles.OpsAdminRestricted + | AuthorizationRoles.OpsAdmin, ReadFullRecord = AuthorizationRoles.BizAdminRestricted + | AuthorizationRoles.BizAdmin + | AuthorizationRoles.ServiceRestricted + | AuthorizationRoles.Service + | AuthorizationRoles.InventoryRestricted + | AuthorizationRoles.Inventory + | AuthorizationRoles.Accounting + | AuthorizationRoles.TechRestricted + | AuthorizationRoles.Tech + | AuthorizationRoles.SubContractorRestricted + | AuthorizationRoles.SubContractor + | AuthorizationRoles.Sales + | AuthorizationRoles.SalesRestricted + | AuthorizationRoles.OpsAdminRestricted + | AuthorizationRoles.OpsAdmin, + Select = AuthorizationRoles.BizAdminRestricted + | AuthorizationRoles.BizAdmin + | AuthorizationRoles.ServiceRestricted + | AuthorizationRoles.Service + | AuthorizationRoles.InventoryRestricted + | AuthorizationRoles.Inventory + | AuthorizationRoles.Accounting + | AuthorizationRoles.TechRestricted + | AuthorizationRoles.Tech + | AuthorizationRoles.SubContractorRestricted + | AuthorizationRoles.SubContractor + | AuthorizationRoles.Sales + | AuthorizationRoles.SalesRestricted + | AuthorizationRoles.OpsAdminRestricted + | AuthorizationRoles.OpsAdmin, }); //////////////////////////////////////////////////////////////////// diff --git a/server/AyaNova/biz/IntegrationBiz.cs b/server/AyaNova/biz/IntegrationBiz.cs new file mode 100644 index 00000000..e1753c72 --- /dev/null +++ b/server/AyaNova/biz/IntegrationBiz.cs @@ -0,0 +1,193 @@ +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using AyaNova.Util; +using AyaNova.Api.ControllerHelpers; +using AyaNova.Models; + +namespace AyaNova.Biz +{ + internal class IntegrationBiz : BizObject + { + internal IntegrationBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles) + { + ct = dbcontext; + UserId = currentUserId; + UserTranslationId = userTranslationId; + CurrentUserRoles = UserRoles; + BizType = AyaType.Integration; + } + + internal static IntegrationBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null) + { + if (httpContext != null) + return new IntegrationBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items)); + else + return new IntegrationBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //EXISTS + internal async Task ExistsAsync(long id) + { + return await ct.Integration.AnyAsync(z => z.Id == id); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //CREATE + // + internal async Task CreateAsync(Integration newObject) + { + await ValidateAsync(newObject, null); + if (HasErrors) + return null; + else + { + + await ct.Integration.AddAsync(newObject); + await ct.SaveChangesAsync(); + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct); + return newObject; + } + } + + + //////////////////////////////////////////////////////////////////////////////////////////////// + //GET + // + internal async Task GetAsync(long id, bool logTheGetEvent = true, bool populatePartNames = false) + { + var ret = await ct.Integration.AsNoTracking().Include(z => z.Items).SingleOrDefaultAsync(m => m.Id == id); + if (logTheGetEvent && ret != null) + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, AyaEvent.Retrieved), ct); + + return ret; + } + + + //////////////////////////////////////////////////////////////////////////////////////////////// + //UPDATE + // + internal async Task PutAsync(Integration putObject) + { + //Get the db object with no tracking as about to be replaced not updated + Integration dbObject = await GetAsync(putObject.Id, false); + if (dbObject == null) + { + AddError(ApiErrorCode.NOT_FOUND, "id"); + return null; + } + if (dbObject.Concurrency != putObject.Concurrency) + { + AddError(ApiErrorCode.CONCURRENCY_CONFLICT); + return null; + } + + + await ValidateAsync(putObject, dbObject); + if (HasErrors) return null; + ct.Replace(dbObject, putObject); + try + { + await ct.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!await ExistsAsync(putObject.Id)) + AddError(ApiErrorCode.NOT_FOUND); + else + AddError(ApiErrorCode.CONCURRENCY_CONFLICT); + return null; + } + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, BizType, AyaEvent.Modified), ct); + + + return putObject; + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //DELETE + // + internal async Task DeleteAsync(long id) + { + using (var transaction = await ct.Database.BeginTransactionAsync()) + { + Integration dbObject = await GetAsync(id, false); + if (dbObject == null) + { + AddError(ApiErrorCode.NOT_FOUND); + return false; + } + ValidateCanDelete(dbObject); + if (HasErrors) + return false; + + ct.Integration.Remove(dbObject); + await ct.SaveChangesAsync(); + + //Log event + await EventLogProcessor.DeleteObjectLogAsync(UserId, BizType, dbObject.Id, dbObject.Name, ct); + await transaction.CommitAsync(); + + return true; + } + } + + + + + + //////////////////////////////////////////////////////////////////////////////////////////////// + //VALIDATION + // + private async Task ValidateAsync(Integration proposedObj, Integration currentObj) + { + + bool isNew = currentObj == null; + + //Name required + if (string.IsNullOrWhiteSpace(proposedObj.Name)) + AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name"); + + + //If name is otherwise OK, check that name is unique + if (!PropertyHasErrors("Name")) + { + //Use Any command is efficient way to check existance, it doesn't return the record, just a true or false + if (await ct.Integration.AnyAsync(m => m.Name == proposedObj.Name && m.Id != proposedObj.Id)) + { + AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name"); + } + } + + //Name required + if (proposedObj.IntegrationAppId == Guid.Empty) + AddError(ApiErrorCode.VALIDATION_REQUIRED, "IntegrationAppId"); + + //If name is otherwise OK, check that name is unique + if (!PropertyHasErrors("IntegrationAppId")) + { + //Use Any command is efficient way to check existance, it doesn't return the record, just a true or false + if (await ct.Integration.AnyAsync(m => m.IntegrationAppId == proposedObj.IntegrationAppId && m.Id != proposedObj.Id)) + { + AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "IntegrationAppId"); + } + } + + } + + private void ValidateCanDelete(Integration inObj) + { + //whatever needs to be check to delete this object + } + + + + + ///////////////////////////////////////////////////////////////////// + + }//eoc + + +}//eons + diff --git a/server/AyaNova/models/Integration.cs b/server/AyaNova/models/Integration.cs index bf914700..c63e96f5 100644 --- a/server/AyaNova/models/Integration.cs +++ b/server/AyaNova/models/Integration.cs @@ -14,7 +14,7 @@ namespace AyaNova.Models public long Id { get; set; } public uint Concurrency { get; set; } [Required] - public Guid IntegrationAppId { get; set; }//Guid of integrating application. All data for it is stored under this guid key. This guid should be fixed and unchanged for all users of the integration app, i.e. it shouldn't be unique to each install but unique to itself once, a publish app id + public Guid IntegrationAppId { get; set; } = Guid.Empty;//Guid of integrating application. All data for it is stored under this guid key. This guid should be fixed and unchanged for all users of the integration app, i.e. it shouldn't be unique to each install but unique to itself once, a publish app id [Required] public string Name { get; set; } public bool Active { get; set; }//integration apps should always check if this is true before working or not and give appropriate error if turned off