diff --git a/devdocs/todo.txt b/devdocs/todo.txt index 85713b20..175ad522 100644 --- a/devdocs/todo.txt +++ b/devdocs/todo.txt @@ -75,6 +75,7 @@ TODO CLIENT STUFF TODO SERVER STUFF - Add license violation check - valid number of techs for license in multiple places + - do a big seed and see if it fails - Trial license ROCKFISH up it to 1000 techs to cover huge seeding. diff --git a/server/AyaNova/Startup.cs b/server/AyaNova/Startup.cs index 5bdeb77a..2ad8611b 100644 --- a/server/AyaNova/Startup.cs +++ b/server/AyaNova/Startup.cs @@ -419,7 +419,7 @@ namespace AyaNova if (TESTING_REFRESH_DB) { AyaNova.Core.License.Fetch(apiServerState, dbContext, _log); - Util.Seeder.SeedDatabase(Util.Seeder.SeedLevel.SmallOneManShopTrialDataSet, -8);//############################################################################################# + Util.Seeder.SeedDatabase(Util.Seeder.SeedLevel.LargeCorporateMultiRegionalTrialDataSet, -8);//############################################################################################# } //TESTING #endif diff --git a/server/AyaNova/biz/JobsBiz.cs b/server/AyaNova/biz/JobsBiz.cs index 27ee98d2..3ef28a37 100644 --- a/server/AyaNova/biz/JobsBiz.cs +++ b/server/AyaNova/biz/JobsBiz.cs @@ -282,7 +282,7 @@ namespace AyaNova.Biz /// Process all jobs (stock jobs and those found in operations table) /// /// - internal static async Task ProcessJobsAsync(AyContext ct) + internal static async Task ProcessJobsAsync(AyContext ct, AyaNova.Api.ControllerHelpers.ApiServerState serverState) { //Flush metrics report before anything else happens log.LogTrace("Flushing metrics to reporters"); @@ -344,7 +344,18 @@ namespace AyaNova.Biz //Health check / metrics await CoreJobMetricsSnapshot.DoJobAsync(ct); - //License check?? + //License check + long CurrentActiveCount = UserBiz.ActiveCount; + long LicensedUserCount = AyaNova.Core.License.ActiveKey.ActiveNumber; + // log.LogInformation("JobsBiz::Checking license active count"); + if (CurrentActiveCount > LicensedUserCount) + { + var msg = $"E1020 - Active count exceeded capacity"; + serverState.SetSystemLock(msg); + log.LogCritical(msg); + return; + } + //Notifications diff --git a/server/AyaNova/biz/UserBiz.cs b/server/AyaNova/biz/UserBiz.cs index bdf462ae..87d03cca 100644 --- a/server/AyaNova/biz/UserBiz.cs +++ b/server/AyaNova/biz/UserBiz.cs @@ -30,9 +30,14 @@ namespace AyaNova.Biz SeedOrImportRelaxedRulesMode = false;//default } - //todo: - //then after that go into widget and anywhere else that there is this associated type code being called for event and search and implement everywhere, - //then update seeder code to use it and get back on to the main critical path again in the todo + //This is where active tech license consumers are accounted for + internal static long ActiveCount + { + get + { + return ServiceProviderProvider.DBContext.User.Where(x => x.Active == true && (x.UserType == UserType.Schedulable || x.UserType == UserType.Subcontractor)).LongCount(); + } + } internal static UserBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext) { @@ -53,35 +58,34 @@ namespace AyaNova.Biz inObj.Salt = Hasher.GenerateSalt(); inObj.Password = Hasher.hash(inObj.Salt, inObj.Password); + inObj.OwnerId = UserId; + inObj.Tags = TagUtil.NormalizeTags(inObj.Tags); + //Seeder sets user options in advance so no need to create them here in that case + if (inObj.UserOptions == null) + inObj.UserOptions = new UserOptions(UserId); + + Validate(inObj, null); if (HasErrors) return null; else { - //do stuff with User - User outObj = inObj; - outObj.OwnerId = UserId; - outObj.Tags = TagUtil.NormalizeTags(outObj.Tags); - //Seeder sets user options in advance so no need to create them here in that case - if (outObj.UserOptions == null) - outObj.UserOptions = new UserOptions(UserId); - await ct.User.AddAsync(outObj); + await ct.User.AddAsync(inObj); //save to get Id await ct.SaveChangesAsync(); //Handle child and associated items //Log event - EventLogProcessor.LogEventToDatabase(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), ct); + EventLogProcessor.LogEventToDatabase(new Event(UserId, inObj.Id, BizType, AyaEvent.Created), ct); - //SEARCH INDEXING - //Search.ProcessNewObjectKeywords( UserLocaleId, outObj.Id, BizType, outObj.Name, outObj.EmployeeNumber, outObj.Notes, outObj.Name); - var SearchParams = new Search.SearchIndexProcessObjectParameters(UserLocaleId, outObj.Id, BizType, outObj.Name); - SearchParams.AddWord(outObj.Notes).AddWord(outObj.Name).AddWord(outObj.EmployeeNumber).AddWord(outObj.Tags); + //SEARCH INDEXING + var SearchParams = new Search.SearchIndexProcessObjectParameters(UserLocaleId, inObj.Id, BizType, inObj.Name); + SearchParams.AddWord(inObj.Notes).AddWord(inObj.Name).AddWord(inObj.EmployeeNumber).AddWord(inObj.Tags); Search.ProcessNewObjectKeywords(SearchParams); - return outObj; + return inObj; } } @@ -93,36 +97,34 @@ namespace AyaNova.Biz //This is a new user so it will have been posted with a password in plaintext which needs to be salted and hashed inObj.Salt = Hasher.GenerateSalt(); inObj.Password = Hasher.hash(inObj.Salt, inObj.Password); + inObj.OwnerId = UserId; + inObj.Tags = TagUtil.NormalizeTags(inObj.Tags); + //Seeder sets user options in advance so no need to create them here in that case + if (inObj.UserOptions == null) + inObj.UserOptions = new UserOptions(UserId); Validate(inObj, null); if (HasErrors) return null; else { - //do stuff with User - User outObj = inObj; - outObj.OwnerId = UserId; - outObj.Tags = TagUtil.NormalizeTags(outObj.Tags); - //Seeder sets user options in advance so no need to create them here in that case - if (outObj.UserOptions == null) - outObj.UserOptions = new UserOptions(UserId); - TempContext.User.Add(outObj); + TempContext.User.Add(inObj); + //save to get Id TempContext.SaveChanges(); //Handle child and associated items //Log event - EventLogProcessor.LogEventToDatabase(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), TempContext); + EventLogProcessor.LogEventToDatabase(new Event(UserId, inObj.Id, BizType, AyaEvent.Created), TempContext); //SEARCH INDEXING - // Search.ProcessNewObjectKeywords(UserLocaleId, outObj.Id, BizType, outObj.Name, outObj.EmployeeNumber, outObj.Notes, outObj.Name); - var SearchParams = new Search.SearchIndexProcessObjectParameters(UserLocaleId, outObj.Id, BizType, outObj.Name); - SearchParams.AddWord(outObj.Notes).AddWord(outObj.Name).AddWord(outObj.EmployeeNumber).AddWord(outObj.Tags); + var SearchParams = new Search.SearchIndexProcessObjectParameters(UserLocaleId, inObj.Id, BizType, inObj.Name); + SearchParams.AddWord(inObj.Notes).AddWord(inObj.Name).AddWord(inObj.EmployeeNumber).AddWord(inObj.Tags); Search.ProcessNewObjectKeywords(SearchParams); - return outObj; + return inObj; } } @@ -229,51 +231,6 @@ namespace AyaNova.Biz //get picklist (paged) internal ApiPagedResponse GetPickList(IUrlHelper Url, string routeName, PagingOptions pagingOptions) { - // pagingOptions.Offset = pagingOptions.Offset ?? PagingOptions.DefaultOffset; - // pagingOptions.Limit = pagingOptions.Limit ?? PagingOptions.DefaultLimit; - - // NameIdItem[] items; - // int totalRecordCount = 0; - - // if (!string.IsNullOrWhiteSpace(q)) - // { - // items = await ct.User - // .AsNoTracking() - // .Where(m => EF.Functions.ILike(m.Name, q)) - // .OrderBy(m => m.Name) - // .Skip(pagingOptions.Offset.Value) - // .Take(pagingOptions.Limit.Value) - // .Select(m => new NameIdItem() - // { - // Id = m.Id, - // Name = m.Name - // }).ToArrayAsync(); - - // totalRecordCount = await ct.User.Where(m => EF.Functions.ILike(m.Name, q)).CountAsync(); - // } - // else - // { - // items = await ct.User - // .AsNoTracking() - // .OrderBy(m => m.Name) - // .Skip(pagingOptions.Offset.Value) - // .Take(pagingOptions.Limit.Value) - // .Select(m => new NameIdItem() - // { - // Id = m.Id, - // Name = m.Name - // }).ToArrayAsync(); - - // totalRecordCount = await ct.User.CountAsync(); - // } - - - - // var pageLinks = new PaginationLinkBuilder(Url, routeName, null, pagingOptions, totalRecordCount).PagingLinksObject(); - - // ApiPagedResponse pr = new ApiPagedResponse(items, pageLinks); - // return pr; - pagingOptions.Offset = pagingOptions.Offset ?? PagingOptions.DefaultOffset; pagingOptions.Limit = pagingOptions.Limit ?? PagingOptions.DefaultLimit; @@ -287,8 +244,6 @@ namespace AyaNova.Biz } - - //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // @@ -301,24 +256,24 @@ namespace AyaNova.Biz inObj.OwnerId = dbObj.OwnerId; //Get a snapshot of the original db value object before changes - User SnapshotObj = new User(); - CopyObject.Copy(dbObj, SnapshotObj); + User SnapshotOfOriginalDBObj = new User(); + CopyObject.Copy(dbObj, SnapshotOfOriginalDBObj); //Update the db object with the PUT object values CopyObject.Copy(inObj, dbObj, "Id, Salt"); dbObj.Tags = TagUtil.NormalizeTags(dbObj.Tags); //Is the user updating the password? - if (!string.IsNullOrWhiteSpace(inObj.Password) && SnapshotObj.Password != inObj.Password) + if (!string.IsNullOrWhiteSpace(inObj.Password) && SnapshotOfOriginalDBObj.Password != inObj.Password) { //YES password is being updated: - dbObj.Password = Hasher.hash(SnapshotObj.Salt, inObj.Password); + dbObj.Password = Hasher.hash(SnapshotOfOriginalDBObj.Salt, inObj.Password); } else { //No, use the snapshot password value - dbObj.Password = SnapshotObj.Password; - dbObj.Salt = SnapshotObj.Salt; + dbObj.Password = SnapshotOfOriginalDBObj.Password; + dbObj.Salt = SnapshotOfOriginalDBObj.Salt; } @@ -326,7 +281,7 @@ namespace AyaNova.Biz //this will allow EF to check it out ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = inObj.ConcurrencyToken; - Validate(dbObj, SnapshotObj); + Validate(dbObj, SnapshotOfOriginalDBObj); if (HasErrors) return false; @@ -349,22 +304,22 @@ namespace AyaNova.Biz if (!ValidateJsonPatch.Validate(this, objectPatch)) 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); + User SnapshotOfOriginalDBObj = new User(); + CopyObject.Copy(dbObj, SnapshotOfOriginalDBObj); //Do the patching objectPatch.ApplyTo(dbObj); dbObj.Tags = TagUtil.NormalizeTags(dbObj.Tags); //Is the user patching the password? - if (!string.IsNullOrWhiteSpace(dbObj.Password) && dbObj.Password != snapshotObj.Password) + if (!string.IsNullOrWhiteSpace(dbObj.Password) && dbObj.Password != SnapshotOfOriginalDBObj.Password) { //YES password is being updated: dbObj.Password = Hasher.hash(dbObj.Salt, dbObj.Password); } ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = concurrencyToken; - Validate(dbObj, snapshotObj); + Validate(dbObj, SnapshotOfOriginalDBObj); if (HasErrors) return false; @@ -424,11 +379,36 @@ namespace AyaNova.Biz bool isNew = currentObj == null; - if (isNew) //Yes, no currentObj - { + //do we need to check the license situation? + if (proposedObj.IsTech && proposedObj.Active) + { + //Yes, it might be affected depending on things + long CurrentActiveCount = UserBiz.ActiveCount; + long LicensedUserCount = AyaNova.Core.License.ActiveKey.ActiveNumber; + + if (isNew) + { + //This operation is about to consume one more license, check that we are not at the limit already + CheckActiveForValidation(CurrentActiveCount, LicensedUserCount); + } + else + { + //did anything that might affect licensing change? + if (!currentObj.IsTech || (!currentObj.Active))//currently not a tech or if it is it's not active + { + //going from non tech to tech and active + //Yes, this is about to consume one more license, check that we are not at the limit already + CheckActiveForValidation(CurrentActiveCount, LicensedUserCount); + } + } } + + // TODO: check user count if not new to see if affected that way + //also check user count in general to see if it's exceeded + //And maybe check it in login as well as a good central spot or wherever makes sense + //OwnerId required if (!isNew) { @@ -543,6 +523,14 @@ namespace AyaNova.Biz return; } + private void CheckActiveForValidation(long CurrentActiveCount, long LicensedUserCount) + { + if (CurrentActiveCount >= LicensedUserCount) + { + AddError(ValidationErrorType.InvalidOperation, null, "LT:ErrorSecurityUserCapacity"); + } + } + //Can delete? private void ValidateCanDelete(User inObj) @@ -560,7 +548,7 @@ namespace AyaNova.Biz //There's only one rule - have they done anything eventlog worthy yet? if (ct.Event.Select(m => m).Where(m => m.OwnerId == inObj.Id).Count() > 0) { - AddError(ValidationErrorType.InvalidOperation, "user", "[E_ACTIVE_NOT_DELETABLE] This user shows activity in the database and can not be deleted. Set inactive instead."); + AddError(ValidationErrorType.InvalidOperation, "user", "LT:ErrorDBForeignKeyViolation"); return; } diff --git a/server/AyaNova/biz/WidgetBiz.cs b/server/AyaNova/biz/WidgetBiz.cs index d1c9e4a2..f71c168d 100644 --- a/server/AyaNova/biz/WidgetBiz.cs +++ b/server/AyaNova/biz/WidgetBiz.cs @@ -332,6 +332,8 @@ namespace AyaNova.Biz //run validation and biz rules if (isNew) { + //WARNING: this is not really the "current" object, it's been modified already by caller + // //NEW widgets must be active // if (inObj.Active == null || ((bool)inObj.Active) == false) // { diff --git a/server/AyaNova/generator/Generate.cs b/server/AyaNova/generator/Generate.cs index 33c53a8c..9baaf9fc 100644 --- a/server/AyaNova/generator/Generate.cs +++ b/server/AyaNova/generator/Generate.cs @@ -86,7 +86,7 @@ namespace AyaNova.Generator //================================================================= try { - await JobsBiz.ProcessJobsAsync(ct); + await JobsBiz.ProcessJobsAsync(ct, serverState); } catch (Exception ex) { diff --git a/server/AyaNova/models/User.cs b/server/AyaNova/models/User.cs index ed0fe75a..9f818222 100644 --- a/server/AyaNova/models/User.cs +++ b/server/AyaNova/models/User.cs @@ -46,7 +46,15 @@ namespace AyaNova.Models public User() { - Tags = new List(); + Tags = new List(); + } + + public bool IsTech + { + get + { + return this.UserType == UserType.Subcontractor || this.UserType == UserType.Schedulable; + } } } diff --git a/server/AyaNova/util/License.cs b/server/AyaNova/util/License.cs index 4eb8ea0a..3747571f 100644 --- a/server/AyaNova/util/License.cs +++ b/server/AyaNova/util/License.cs @@ -104,6 +104,13 @@ namespace AyaNova.Core return null; } + public long ActiveNumber + { + get + { + return GetLicenseFeature(SERVICE_TECHS_FEATURE_NAME).Count; + } + } /// /// Check for the existance of license feature @@ -446,7 +453,7 @@ namespace AyaNova.Core if (ldb.Key == "none") { - var msg = "License key not found in database, running in unlicensed mode"; + var msg = "E1020 - License key not found in database, running in unlicensed mode"; apiServerState.SetSystemLock(msg); log.LogWarning(msg); return; @@ -456,9 +463,9 @@ namespace AyaNova.Core AyaNovaLicenseKey k = Parse(ldb.Key, log); if (k == null) { - var msg = "Error: License key in database is not valid, running in unlicensed mode"; + var msg = "E1020 - License key in database is not valid, running in unlicensed mode"; apiServerState.SetSystemLock(msg); - log.LogError(msg); + log.LogCritical(msg); return; } @@ -466,9 +473,18 @@ namespace AyaNova.Core if (_ActiveLicense.LicenseExpired) { - var msg = $"License key expired {DateUtil.ServerDateTimeString(_ActiveLicense.LicenseExpiration)}"; + var msg = $"E1020 - License key expired {DateUtil.ServerDateTimeString(_ActiveLicense.LicenseExpiration)}"; apiServerState.SetSystemLock(msg); - log.LogWarning(msg); + log.LogCritical(msg); + return; + } + + //Has someone been trying funny business with the active techs in the db? + if (AyaNova.Biz.UserBiz.ActiveCount > _ActiveLicense.ActiveNumber) + { + var msg = $"E1020 - Active count exceeded capacity"; + apiServerState.SetSystemLock(msg); + log.LogCritical(msg); return; } @@ -484,7 +500,8 @@ namespace AyaNova.Core catch (Exception ex) { var msg = "E1020 - Error initializing license key"; - log.LogError(ex, msg); + log.LogCritical(ex, msg); + apiServerState.SetSystemLock(msg); throw new ApplicationException(msg, ex); } } @@ -503,7 +520,7 @@ namespace AyaNova.Core if (ParsedNewKey == null) { - throw new ApplicationException("License.Install -> key could not be parsed"); + throw new ApplicationException("E1020 - License.Install -> key could not be parsed"); } //Can't install a trial into a non-empty db @@ -512,6 +529,12 @@ namespace AyaNova.Core throw new ApplicationException("E1020 - Can't install a trial key into a non empty AyaNova database. Erase the database first."); } + //TODO: TECHCOUNT - new license causes exceeding count? + if (AyaNova.Biz.UserBiz.ActiveCount > ParsedNewKey.GetLicenseFeature(SERVICE_TECHS_FEATURE_NAME).Count) + { + throw new ApplicationException("E1020 - Can't install key, too many active techs and / or subcontractors in database. Deactivate enough to install key."); + } + //Update current license CurrentInDbKeyRecord.Key = RawTextNewKey; //LOOKAT: reason, resultcode etc @@ -547,7 +570,7 @@ namespace AyaNova.Core if (string.IsNullOrWhiteSpace(k)) { - throw new ApplicationException("License.Parse -> License key is empty and can't be validated"); + throw new ApplicationException("E1020 - License.Parse -> License key is empty and can't be validated"); } try @@ -557,7 +580,7 @@ namespace AyaNova.Core !k.Contains("[SIGNATURE") || !k.Contains("SIGNATURE]")) { - throw new ApplicationException("License.Parse -> License key is missing required delimiters"); + throw new ApplicationException("E1020 - License.Parse -> License key is missing required delimiters"); } @@ -588,7 +611,7 @@ EQIDAQAB signer.BlockUpdate(msgBytes, 0, msgBytes.Length); if (!signer.VerifySignature(expectedSig)) { - throw new ApplicationException("License.Parse -> License key failed integrity check and is not valid"); + throw new ApplicationException("E1020 - License.Parse -> License key failed integrity check and is not valid"); } #endregion check signature @@ -598,7 +621,7 @@ EQIDAQAB key.LicenseFormat = (string)token.SelectToken("Key.LicenseFormat"); if (key.LicenseFormat != "2018") - throw new ApplicationException($"License.Parse -> License key format {key.LicenseFormat} not recognized"); + throw new ApplicationException($"E1020 - License.Parse -> License key format {key.LicenseFormat} not recognized"); key.Id = (string)token.SelectToken("Key.Id"); key.RegisteredTo = (string)token.SelectToken("Key.RegisteredTo"); key.DbId = (Guid)token.SelectToken("Key.DBID");