using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using NHibernate; using NHibernate.Linq; using Sleis.Models; using Sleis.Infrastructure; using log4net; using Sleis.Utility; using Sleis.Data; using System.Security; using Sleis.Models.ErrorHandling; using System.Web.Security; namespace Sleis.Service { public class UserService : BaseService, Sleis.Service.IUserService { //public UserData UserData { get; set; } public RoleData RoleData { get; set; } public UserContactData UserContactData { get; set; } public UserAnswerData UserAnswerData { get; set; } public PasswordData PasswordData { get; set; } public string UserCreateTemplate { get; set; } public string PasswordResetTemplate { get; set; } public string PasswordForgotTemplate { get; set; } public string SecurityQuestionsResetTemplate { get; set; } public int UserSecQuestionListSize { get; set; } public string UserSecQuestionPrefix { get; set; } private int _maxPasswordAttempts = 3; public int MaxPasswordAttempts { get { return _maxPasswordAttempts; } set { _maxPasswordAttempts = value; } } private int _maxSecurityQuestionAttempts = 3; public int MaxSecurityQuestionAttempts { get { return _maxSecurityQuestionAttempts; } set { _maxSecurityQuestionAttempts = value; } } public string UserAccountLockedTemplate { get; set; } public string UserAccountUnlockedTemplate { get; set; } public string UserAccountInactivatedTemplate { get; set; } public string UserAccountActivatedTemplate { get; set; } public static readonly List UserSecQuestionList = new List(); public new void Init() { base.Init(); ArgumentValidationUtility.ThrowOnNull(UserData, "UserData"); ArgumentValidationUtility.ThrowOnNull(RoleData, "RoleData"); ArgumentValidationUtility.ThrowOnNull(UserContactData, "UserContactData"); ArgumentValidationUtility.ThrowOnNull(UserAnswerData, "UserAnswerData"); ArgumentValidationUtility.ThrowOnNull(PasswordData, "PasswordData"); ArgumentValidationUtility.ThrowOnEmpty(UserCreateTemplate, "UserCreateTemplate"); ArgumentValidationUtility.ThrowOnEmpty(PasswordResetTemplate, "PasswordresetTemplate"); ArgumentValidationUtility.ThrowOnEmpty(PasswordForgotTemplate, "PasswordForgotTemplate"); ArgumentValidationUtility.ThrowOnEmpty(SecurityQuestionsResetTemplate, "SecurityQuestionsResetTemplate"); ArgumentValidationUtility.ThrowOnNull(EmailUtility, "EmailUtility"); ArgumentValidationUtility.ThrowOnDefault(UserSecQuestionListSize, "UserSecQuestionListSize"); ArgumentValidationUtility.ThrowOnEmpty(UserSecQuestionPrefix, "UserSecQuestionPrefix"); if (UserSecQuestionList.Count.Equals(0)) { UserSecQuestionList.Add(String.Empty); foreach (KeyValuePair prop in Properties.ExposedProps) { if (prop.Key.StartsWith(UserSecQuestionPrefix)) { UserSecQuestionList.Add(prop.Value); } } } } public void Forgot(string email, string cntrlUri) { ArgumentValidationUtility.ThrowOnEmpty(email, "email"); ArgumentValidationUtility.ThrowOnEmpty(cntrlUri, "cntrlUri"); UserModel usr = UserData.GetByEmail(email); ArgumentValidationUtility.ThrowOnNull(usr, "Invalid email"); //if (usr.Status != SleisUserStatusType.Active && usr.Status != SleisUserStatusType.Pending) if (usr.Status != SleisUserStatusType.Active) { throw new ApplicationException("Invalid user state: " + usr.Status); } //prep message var secret = EncryptionUtility.EncryptTokenForUrl(usr.Id); var fullVerifiUri = cntrlUri + secret; Dictionary emailArgs = new Dictionary(); emailArgs.Add("Name", usr.FullName); emailArgs.Add("Link", fullVerifiUri); //Send email EmailUtility.SendArgs(usr.Email, PasswordForgotTemplate, emailArgs); Audit(new AppEventModel(EventType.Audit, SessionUtility.CurrentUser != null ? SessionUtility.CurrentUser.Email : usr.Email, "Reset Password Request. User: " + usr.Email, HttpContext.Current.Request.UserHostAddress)); } public void ResetUserStatus(int id) { ArgumentValidationUtility.ThrowOnDefault(id, "id"); UserModel usr = UserData.GetById(id); ArgumentValidationUtility.ThrowOnNull(usr, "Invalid user Id"); using (ISession session = PasswordData.GetSession()) { using (var txn = session.BeginTransaction()) { //Reset active passwrod PasswordData.EndAllPassword(session, id); //Change status usr.Status = SleisUserStatusType.Active; UserData.Update(usr, session); //Commit only if everythig else went well txn.Commit(); } } } public UserModel ChangePassword(int id, string password) { ArgumentValidationUtility.ThrowOnDefault(id, "id"); ArgumentValidationUtility.ThrowOnEmpty(password, "password"); UserModel usr = UserData.GetById(id); ArgumentValidationUtility.ThrowOnNull(usr, "Invalid user Id"); if (usr.Status != SleisUserStatusType.Active) { throw new ApplicationException("Invalid user state."); } using (ISession session = PasswordData.GetSession()) { using (var txn = session.BeginTransaction()) { List oldPasswords = PasswordData.Get(x => x.UserId == id).ToList(); if(oldPasswords.Exists(x=>x.Password == EncryptionUtility.OneWayHash(password))) { throw new SleisPasswordException(); } //Reset active passwrod PasswordData.EndAllPassword(session, id); SimplePasswordModel pass = new SimplePasswordModel(); pass.UserId = usr.Id; pass.Password = EncryptionUtility.OneWayHash(password); pass.On = null; //Save a new password session.Save(pass); //Change status usr.Status = SleisUserStatusType.Active; UserData.Update(usr, session); //prep message Dictionary emailArgs = new Dictionary(); emailArgs.Add("Name", usr.FullName); //Commit only if everythig else went well txn.Commit(); //send success email. try { EmailUtility.SendArgs(usr.Email, PasswordResetTemplate, emailArgs); } catch(Exception err) { Log.Error("Change password success email failed to send. ", err); //swallowed. Not really critical to error whole app if email fails. user will still be able to login with new password just fine. } } } return usr; } public void RetirePassword(UserModel user) { using (ISession session = PasswordData.GetSession()) { //Reset active passwrod PasswordData.EndAllPassword(session, user.Id); session.Flush(); //persist } } public void ResetSecurityQuestions(UserModel user) { if (user.ContactSecAnswers == null || user.ContactSecAnswers.Count <= 0) LoadUserExtendedProperties(user, 0); foreach (var a in user.ContactSecAnswers) { a.EndDate = DateTime.Now; a.Question = EncryptionUtility.TripleDESEncrypt(a.Question); a.Answer = EncryptionUtility.TripleDESEncrypt(a.Answer); UserAnswerData.SaveOrUpdate(a); } Dictionary emailArgs = new Dictionary(); emailArgs.Add("Name", user.FullName); //Send email EmailUtility.SendArgs(user.Email, SecurityQuestionsResetTemplate, emailArgs); Audit(new AppEventModel(EventType.Audit, SessionUtility.CurrentUser.Email, "Reset Security Questions For User: " + user.Email, HttpContext.Current.Request.UserHostAddress)); } public UserModel UpdateFacilityUser(UserModel user, int facilityId) { ArgumentValidationUtility.ThrowOnNull(user, "user"); //ArgumentValidationUtility.ThrowOnDefault(facilityId, "facilityId"); using (ISession session = UserData.GetSession()) { //If trying to remove administrator access, make sure the user is not the last Admin at this Facility. if (!user.Roles.Contains(AppUserRoleType.FacilityAdmin) && facilityId >0) { if (RoleData.IsLastFacilityAdmin(session, user.Id, facilityId)) { return null; } } int revCounter = 0; user.ContactPoints = user.ContactPoints.Where(x => !String.IsNullOrEmpty(x.Value)).ToList(); foreach (UserContactModel contact in user.ContactPoints) { contact.UserId = user.Id; contact.Index = ++revCounter; } using (var txn = session.BeginTransaction()) { //save session.Update(user); //Contacts UserContactData.SaveUserContacts(session, user.Id, user.ContactPoints); //only update role data if facilityId has a value or is agency user. otherwise we are updating partial user record. if (facilityId > 0 || user.Type == SleisUserTypeType.Agency) { RoleData.SaveRoles(session, user.Id, facilityId, user.Roles); } //Commit only if everythig else went well txn.Commit(); } } //decrypt after save foreach (UserAnswerModel answer in user.ContactSecAnswers) { answer.Answer = EncryptionUtility.TripleDESDecrypt(answer.Answer); answer.Question = EncryptionUtility.TripleDESDecrypt(answer.Question); } return user; } public UserModel UpdateUserProfile(UserModel user, UserModel profile) { ArgumentValidationUtility.ThrowOnNull(user, "user"); ArgumentValidationUtility.ThrowOnNull(profile, "profile"); user.FullName = profile.FullName; user.Org = profile.Org; user.Title = profile.Title; int revCounter = 0; user.ContactPoints = profile.ContactPoints.Where(x => !String.IsNullOrEmpty(x.Value)).ToList(); foreach (UserContactModel contact in user.ContactPoints) { contact.UserId = user.Id; contact.Index = ++revCounter; } //Encrypt before save revCounter = 0; user.ContactSecAnswers = profile.ContactSecAnswers; foreach (UserAnswerModel answer in user.ContactSecAnswers) { if (answer.Answer == "") break; answer.UserId = user.Id; answer.Index = ++revCounter; answer.Question = EncryptionUtility.TripleDESEncrypt(answer.Question); answer.Answer = EncryptionUtility.TripleDESEncrypt(answer.Answer); } using (ISession session = UserData.GetSession()) { using (var txn = session.BeginTransaction()) { Log.Info("Saving user."); //save session.Update(user); //Contacts UserContactData.SaveUserContacts(session, user.Id, user.ContactPoints); //check to make sure we are truly saving new question ansers if (UserAnswerData.Count(x=> x.UserId == user.Id && x.EndDate == null) == 0) { //Answers UserAnswerData.SaveUserAnswers(session, user.Id, user.ContactSecAnswers); //Audit Audit(new AppEventModel(EventType.Audit, SessionUtility.CurrentUser.Email, "User Profile Challenge Questions Answers Set", HttpContext.Current.Request.UserHostAddress)); } //Commit only if everythig else went well txn.Commit(); } } //decrypt after save foreach (UserAnswerModel answer in user.ContactSecAnswers) { if (answer.Answer == "") break; answer.Answer = EncryptionUtility.TripleDESDecrypt(answer.Answer); answer.Question = EncryptionUtility.TripleDESDecrypt(answer.Question); } return user; } public void UpdateUser(UserModel obj, int? facilityId) { ArgumentValidationUtility.ThrowOnNull(obj, "obj"); using (ISession session = UserData.GetSession()) { using (var txn = session.BeginTransaction()) { //save session.Update(obj); //Contacts UserContactData.SaveUserContacts(session, obj.Id, obj.ContactPoints); //Roles //This should update only the facility roles is facilityId != null and only app roles if facilityId == null RoleData.SaveRoles(session, obj.Id, facilityId, obj.Roles); //Answers UserAnswerData.SaveUserAnswers(session, obj.Id, obj.ContactSecAnswers); //Commit only if everythig else went well txn.Commit(); } } } public void UpdateUserStatus(int userId, SleisUserStatusType status) { UpdateUserStatus(UserData.GetSingle(x => x.Id == userId), status); } public void UpdateUserStatus(UserModel user, SleisUserStatusType status) { var emailTemplate = ""; var action = ""; if (status == SleisUserStatusType.Locked) { emailTemplate = UserAccountLockedTemplate; action = "Locking user account. User: " + user.Email; } else if (user.Status == SleisUserStatusType.Locked && status == SleisUserStatusType.Active) { emailTemplate = UserAccountUnlockedTemplate; action = "Unlocking user account. User: " + user.Email; //reset user failed attempt counts user.FailedPasswordAttemptCount = 0; user.FailedSecurityQuestionAnswerCount = 0; } else if (status == SleisUserStatusType.Inactive) { emailTemplate = UserAccountInactivatedTemplate; action = "Deactivating user account. User: " + user.Email; } else if (user.Status == SleisUserStatusType.Inactive && status == SleisUserStatusType.Active) { emailTemplate = UserAccountActivatedTemplate; action = "Activating user account. User: " + user.Email; } //update user status and save user.Status = status; UserData.Update(user); Dictionary emailArgs = new Dictionary(); emailArgs.Add("Name", user.FullName); //Send email EmailUtility.SendArgs(user.Email, emailTemplate, emailArgs); //Audit Audit(new AppEventModel(EventType.Audit, SessionUtility.CurrentUser!= null ? SessionUtility.CurrentUser.Email : user.Email, action, HttpContext.Current.Request.UserHostAddress)); } public void CreateUser(UserModel obj, int? facilityId, string cntrlUri) { ArgumentValidationUtility.ThrowOnNull(obj, "obj"); ArgumentValidationUtility.ThrowOnEmpty(cntrlUri, "confirmUrl"); using (ISession session = UserData.GetSession()) { using (var txn = session.BeginTransaction()) { //set the user status to pending obj.Status = SleisUserStatusType.Active; //save session.Save(obj); //Add Contacts UserContactData.SaveUserContacts(session, obj.Id, obj.ContactPoints); //Roles //This should update only the facility roles is facilityId != null and only app roles if facilityId == null RoleData.SaveRoles(session, obj.Id, facilityId, obj.Roles); //prep message var secret = EncryptionUtility.EncryptTokenForUrl(obj.Id); var fullVerifiUri = cntrlUri + secret; Dictionary emailArgs = new Dictionary(); emailArgs.Add("Name", obj.FullName); emailArgs.Add("Link", fullVerifiUri); //Send email EmailUtility.SendArgs(obj.Email, UserCreateTemplate, emailArgs); //Commit only if everythig else went well txn.Commit(); } } } public UserModel AuthUser(SimpleAuthModel auth) { ArgumentValidationUtility.ThrowOnNull(auth, "auth"); ArgumentValidationUtility.ThrowOnEmpty(auth.EmailAddress, "auth.Email"); ArgumentValidationUtility.ThrowOnEmpty(auth.Password, "auth.Password"); UserModel user = UserData.GetByEmail(auth.EmailAddress); if (user == null || user.Status==SleisUserStatusType.Deleted) { //When an exception vs. bug than throw AccessViolationException throw new ApplicationException("Invalid email"); } string systemPass = PasswordData.GetCurrent(user.Id); ArgumentValidationUtility.ThrowOnEmpty(systemPass, "No valid password defined"); string userPass = EncryptionUtility.OneWayHash(auth.Password); if (systemPass != userPass) { LogFailedPasswordAttempt(user); //When an exception vs. bug than throw AccessViolationException throw new ApplicationException("Invalid credentails"); } if (user.Status != SleisUserStatusType.Active) { throw new ApplicationException("Invalid user state"); } LoadUserExtendedProperties(user); user.LastLogin = DateTime.Now; user.FailedPasswordAttemptCount = 0; //reset failed attempts UserData.Update(user); return user; } public bool ValidateUserSecurityQuestionAnswer(UserModel user, string question, string answer) { //First check if they did not tinker with the question foreach (UserAnswerModel a in user.ContactSecAnswers) { if (StringUtility.NoiseTolerantCompare(a.Question, question)) { if (!StringUtility.NoiseTolerantCompare(a.Answer, answer)) { LogFailedSecurityQuestionAttempt(user); return false; } user.FailedSecurityQuestionAnswerCount = 0; //reset UserData.Update(user); return true; } } return false; } private void LoadUserExtendedProperties(List users) { foreach (UserModel user in users) { LoadUserExtendedProperties(user); } } private void LoadUserExtendedProperties(UserModel user) { LoadUserExtendedProperties(user, 0); user.Facilities = FacilityData.GetUserFacilities(user.Id); } private void LoadUserExtendedProperties(UserModel user, int currentFacilityId) { if (user != null) { user.Roles = RoleData.GetByUserId(user.Id); user.AllRoles = RoleData.GetAllByUserId(user.Id); if (currentFacilityId > 0) { user.Roles.AddRange(RoleData.GetFacilityRoles(user.Id, currentFacilityId)); } user.ContactPoints = UserContactData.GetByUserId(user.Id); user.ContactSecAnswers = UserAnswerData.Get(x => x.UserId == user.Id && x.EndDate == null).ToList(); foreach (UserAnswerModel answer in user.ContactSecAnswers) { answer.Answer = EncryptionUtility.TripleDESDecrypt(answer.Answer); answer.Question = EncryptionUtility.TripleDESDecrypt(answer.Question); } } } public UserModel GetUser(int id) { return GetUser(id, 0); } public UserModel GetUser(int id, int currentFacilityId) { ArgumentValidationUtility.ThrowOnDefault(id, "Id"); UserModel user = UserData.GetById(id); LoadUserExtendedProperties(user, currentFacilityId); return user; } public UserModel GetUser(string username, int currentFacilityId) { ArgumentValidationUtility.ThrowOnNull(username, "Username"); UserModel user = UserData.GetSingle(x => x.Email == username); LoadUserExtendedProperties(user, currentFacilityId); return user; } public List GetAgencyUsers() { List users = UserData.Get(u => u.Type == SleisUserTypeType.Agency && u.Status!=SleisUserStatusType.Deleted).ToList(); LoadUserExtendedProperties(users); return users; } public List GetFacilityUsers() { List users = UserData.Get(u => u.Type == SleisUserTypeType.Facility && u.Status!=SleisUserStatusType.Deleted).ToList(); LoadUserExtendedProperties(users); return users; } public List GetUsers() { return UserData.GetAll().ToList(); } public void DeleteUser(int userId) { using (ISession session = UserData.GetSession()) { using (ITransaction trans = session.BeginTransaction()) { UserModel user = GetUser(userId); user.Status = SleisUserStatusType.Deleted; UserData.Update(user); List roles = RoleData.Get(x => x.UserId == userId).ToList(); foreach (UserRoleModel r in roles) { RoleData.Delete(r, session); } trans.Commit(); } } } public void DeleteUserRoles(int userId, int facilityId) { using (ISession session = RoleData.GetSession()) { using (ITransaction trans = session.BeginTransaction()) { RoleData.DeleteUserRoles(session, userId, facilityId); trans.Commit(); } } } public void LogFailedPasswordAttempt(UserModel user) { //Audit Audit(new AppEventModel(EventType.Audit, user.Email, "Failed password attempt", HttpContext.Current.Request.UserHostAddress)); user.FailedPasswordAttemptCount++; if (user.FailedPasswordAttemptCount >= MaxPasswordAttempts) { UpdateUserStatus(user, SleisUserStatusType.Locked); //logout user FormsAuthentication.SignOut(); HttpContext.Current.Session.Clear(); throw new AccountLockedException(); } else UserData.Update(user); } public void LogFailedSecurityQuestionAttempt(UserModel user) { //Audit Audit(new AppEventModel(EventType.Audit, user.Email, "Failed challenge question attempt", HttpContext.Current.Request.UserHostAddress)); user.FailedSecurityQuestionAnswerCount++; if (user.FailedSecurityQuestionAnswerCount >= MaxSecurityQuestionAttempts) { UpdateUserStatus(user, SleisUserStatusType.Locked); //logout user FormsAuthentication.SignOut(); HttpContext.Current.Session.Clear(); throw new AccountLockedException(); } else UserData.Update(user); } } }