diff --git a/Config/Entities/User.cs b/Config/Entities/User.cs index f969c9501..e39736ed4 100644 --- a/Config/Entities/User.cs +++ b/Config/Entities/User.cs @@ -1,5 +1,5 @@ /// -/// Copyright (c) 2013-2017 Sensus Metering Systems +/// Copyright (c) 2013-2019 Sensus Slovensko a.s. /// using System; using System.Collections.Generic; @@ -11,8 +11,11 @@ namespace Config.Entities public virtual int Id { get; protected set; } public virtual string UserName { get; set; } /// = name, alias, abbreviation public virtual string FullName { get; set; } /// = description - public virtual int Number { get; set; } + public virtual string Tag { get; set; } + public virtual int Number { get; set; } public virtual string Password { get; set; } + public virtual string Password2 { get; set; } + public virtual string Password3 { get; set; } public virtual DateTime LastPwChange { get; set; } public virtual IList Groups { get; set; } //User can be a member of a list of groups diff --git a/TBF/BenchControl/BackupAndSecurity/Default/Component.cs b/TBF/BenchControl/BackupAndSecurity/Default/Component.cs new file mode 100644 index 000000000..7d45e20e7 --- /dev/null +++ b/TBF/BenchControl/BackupAndSecurity/Default/Component.cs @@ -0,0 +1,38 @@ +/// +/// Copyright (c) 2019 Sensus Slovensko a.s. +/// +using System; +using Users; +using log4net; + +namespace TBF.BenchControl.BackupAndSecurity.Default +{ + /// + /// Holds backup and security options. + /// + public class Component : ComponentBase, GenericDevices.IBackupAndSecurity + { + private static readonly ILog log = LogManager.GetLogger(typeof(Component)); + public override string ToString() { return string.Format("BackupAndSecurity.Default({0})", Cfg.ToString(1)); } + + readonly ComponentCfg myCfg; + + public int MinPasswdLength { get { return myCfg.MinPasswdLength; } } + public int PasswdExpirationPeriodDays { get { return myCfg.PasswdExpirationPeriodDays; } } + + public Component() + { + } + + public Component(Generic.IComponentCfg cfg) + : base(cfg) + { + myCfg = cfg as ComponentCfg; + + GlobalData.MinPasswdLength = myCfg.MinPasswdLength; + GlobalData.PasswdExpirationPeriodDays = myCfg.PasswdExpirationPeriodDays; + + log.Debug(this.ToString()); + } + } +} diff --git a/TBF/BenchControl/BackupAndSecurity/Default/ComponentCfg.cs b/TBF/BenchControl/BackupAndSecurity/Default/ComponentCfg.cs new file mode 100644 index 000000000..5b3b4162c --- /dev/null +++ b/TBF/BenchControl/BackupAndSecurity/Default/ComponentCfg.cs @@ -0,0 +1,125 @@ +/// +/// Copyright (c) 2019 Sensus Slovensko a.s. +/// +using System; +using System.Collections.Generic; +using System.Xml.Serialization; +using Config.Entities; +using TBF.BenchControl.Generic; + +namespace TBF.BenchControl.BackupAndSecurity.Default +{ + /// + /// Holds backup and security options - serializable configuration. + /// + public class ComponentCfg : ComponentCfgBase, Generic.IComponentCfg, Config.Entities.IParamsProvider + { + public static XmlSerializer Serializer = XmlSerializer.FromTypes(new[] { typeof(ComponentCfg) })[0]; + public override XmlSerializer GetSerializer() { return Serializer; } + + public IComponentCfgCtrl GetControl() { return new Configs.ParamsProvider.ComponentCfgCtrl(this, null); } + + /// + /// Serialized parameters + /// + public int MinPasswdLength; + public int PasswdExpirationPeriodDays; /// 0 = validity is not limited + + /// Private parameterless constructor invoked by all other (public) constructors + ComponentCfg() {} + + public ComponentCfg(IComponentFactory factory) + : this() + { + Factory = factory; + Name = "BackupAndSecurity"; + ParentName = string.Empty; + InitializeAll(); + } + + public string ComponentName { get { return Name; } } + + public void InitializeAll() + { + MinPasswdLength = 6; + PasswdExpirationPeriodDays = 0; + } + + string[] paramNames = new string[] + { + "Minimum password length (characters)", + "Password expiration period (days)", + }; + public string ParamName(int i) { return paramNames[i]; } + public int ParamsCount() { return paramNames.Length; } + + public IList ParamValues(int i) + { + return null; + } + + public string ToString(int i) + { + if (i == -1) + { + return string.Format("{0}: Min. password length = {1} characters, Password expiration period = {2} days", Name, MinPasswdLength, PasswdExpirationPeriodDays); + } + + switch (i) + { + case 0: return MinPasswdLength.ToString(); + case 1: return PasswdExpirationPeriodDays.ToString();; + default: return string.Empty; + } + } + + public CfgUpdateFlags UpdateParam(int i, string strValue) + { + switch (i) + { + case 0: MinPasswdLength = int.Parse(strValue); return CfgUpdateFlags.RestartRqrd; + case 1: PasswdExpirationPeriodDays = int.Parse(strValue); return CfgUpdateFlags.RestartRqrd; + default: return CfgUpdateFlags.None; + } + } + + public bool ValidateParam(int i, string strValue, out string message) + { + message = string.Empty; + int idummy; + + switch (i) + { + case 0: + case 1: + /// Positive integers and zero are allowed + if (int.TryParse(strValue, out idummy) && (idummy >= 0)) return true; + break; + default: + message = "Invalid index"; + return false; + } + + message = ParamName(i) + " is invalid"; + return false; + } + + void CopyContentTo(ComponentCfg prms) + { + prms.MinPasswdLength = this.MinPasswdLength; + prms.PasswdExpirationPeriodDays = this.PasswdExpirationPeriodDays; + } + + public Config.Entities.IParamsProvider Clone() + { + ComponentCfg pars = new ComponentCfg(); + CopyContentTo(pars); + return pars; + } + + public bool UpdateEmbeddedDbEntity() + { + return true; /// =OK, do nothing + } + } +} diff --git a/TBF/BenchControl/BackupAndSecurity/Default/ComponentFactory.cs b/TBF/BenchControl/BackupAndSecurity/Default/ComponentFactory.cs new file mode 100644 index 000000000..a491f092e --- /dev/null +++ b/TBF/BenchControl/BackupAndSecurity/Default/ComponentFactory.cs @@ -0,0 +1,26 @@ +/// +/// Copyright (c) 2019 Sensus Slovensko a.s. +/// +using System.Collections.Generic; +using TBF.BenchControl.Generic; + +namespace TBF.BenchControl.BackupAndSecurity.Default +{ + public class ComponentFactory : IComponentFactory + { + public string ClassName { get { return this.GetType().Namespace.Substring(17); } } + + public void ResetStaticProperties() { Component.ResetStaticProperties(); } + + public IComponent DummyComponent() { return new Component(); } + + public IComponent GetComponent(IComponentCfg cfg, IList components) { return new Component(cfg); } + + public IComponentCfg DefaultConfig() { return new ComponentCfg(this); } + + public IComponentCfg CmpntCfgFromCmpntEntity(Config.Entities.Component component) + { + return ComponentCfgBase.CreateFromDbEntity(ComponentCfg.Serializer, component, this); + } + } +} diff --git a/TBF/BenchControl/GenericDevices/IBackupAndSecurity.cs b/TBF/BenchControl/GenericDevices/IBackupAndSecurity.cs new file mode 100644 index 000000000..05f07e948 --- /dev/null +++ b/TBF/BenchControl/GenericDevices/IBackupAndSecurity.cs @@ -0,0 +1,17 @@ +/// +/// Copyright (c) 2019 Sensus Slovensko a.s. +/// +using Config.Entities; +using TBF.BenchControl.Generic; + +namespace TBF.BenchControl.GenericDevices +{ + /// + /// Backup and security options + /// + public interface IBackupAndSecurity : IComponent + { + int MinPasswdLength { get; } + int PasswdExpirationPeriodDays { get; } + } +} diff --git a/TBF/BenchControl/Sequences/ProcessData.cs b/TBF/BenchControl/Sequences/ProcessData.cs index a76e68d72..572edc1b8 100644 --- a/TBF/BenchControl/Sequences/ProcessData.cs +++ b/TBF/BenchControl/Sequences/ProcessData.cs @@ -7,7 +7,8 @@ namespace TBF.BenchControl.Sequences { public class ProcessData { - public static IBenchInfo BenchInfo; + public static IBackupAndSecurity BackupAndSecurity; + public static IBenchInfo BenchInfo; public static IErrorFlags ErrorFlagsComp; public static Output.DB.SensusOracle.Database OracleDB; public static IList RawTestInfos; /// Incomplete raw test infos from Oracle DB diff --git a/TBF/BenchControl/StateMachine.cs b/TBF/BenchControl/StateMachine.cs index 738ad42f6..1be934bb7 100644 --- a/TBF/BenchControl/StateMachine.cs +++ b/TBF/BenchControl/StateMachine.cs @@ -228,7 +228,8 @@ namespace TBF.BenchControl foreach (var cmpnt in components) { if (cmpnt is Elde.ControlBoardDev) ControlBoard = cmpnt as Elde.ControlBoardDev; - if (cmpnt is IBenchInfo) ProcessData.BenchInfo = cmpnt as IBenchInfo; + if (cmpnt is IBackupAndSecurity) ProcessData.BackupAndSecurity = cmpnt as IBackupAndSecurity; + if (cmpnt is IBenchInfo) ProcessData.BenchInfo = cmpnt as IBenchInfo; if (cmpnt is IErrorFlags) ProcessData.ErrorFlagsComp = cmpnt as IErrorFlags; if ((cmpnt is Output.DB.SensusOracle.Database) && (ProcessData.OracleDB == null)) { diff --git a/TBF/BenchControl/TbfComponents.cs b/TBF/BenchControl/TbfComponents.cs index 6ddf63ff5..3dd6ecca6 100644 --- a/TBF/BenchControl/TbfComponents.cs +++ b/TBF/BenchControl/TbfComponents.cs @@ -15,7 +15,8 @@ namespace TBF.BenchControl { Factories = new List(); - Factories.Add(new BenchInfo.Munich.ComponentFactory()); + Factories.Add(new BackupAndSecurity.Default.ComponentFactory()); + Factories.Add(new BenchInfo.Munich.ComponentFactory()); Factories.Add(new BenchInfo.Extended.ComponentFactory()); Factories.Add(new BenchInfo.iPerl.ComponentFactory()); Factories.Add(new Elde.ControlBoardFactory()); diff --git a/TBF/Properties/AssemblyInfo.cs b/TBF/Properties/AssemblyInfo.cs index 813479281..2816eb06c 100644 --- a/TBF/Properties/AssemblyInfo.cs +++ b/TBF/Properties/AssemblyInfo.cs @@ -29,5 +29,5 @@ using System.Runtime.InteropServices; // Build Number // Revision // -[assembly: AssemblyVersion("2.18.1308.0")] -[assembly: AssemblyFileVersion("2.18.1308.0")] +[assembly: AssemblyVersion("2.18.1313.0")] +[assembly: AssemblyFileVersion("2.18.1313.0")] diff --git a/TBF/TBF.csproj b/TBF/TBF.csproj index 0a87e40dc..c31d315f0 100644 --- a/TBF/TBF.csproj +++ b/TBF/TBF.csproj @@ -158,6 +158,9 @@ AmbientCfgCtrl.cs + + + @@ -588,6 +591,7 @@ ValveCfgCtrl.cs + diff --git a/Users/DB.cs b/Users/DB.cs index a889bb4a8..bb94aa345 100644 --- a/Users/DB.cs +++ b/Users/DB.cs @@ -19,6 +19,7 @@ namespace Users /// Session factory for all regular sessions, not for CreateEmptyResultsDB(). public static ISessionFactory SessionFactory; + public static ISession CurrentSession; /// Connection string for all sessions private static string connectionString; @@ -101,7 +102,8 @@ namespace Users if (SessionFactory == null) SessionFactory = CreateSessionFactory(); - return SessionFactory.OpenSession(); + CurrentSession = SessionFactory.OpenSession(); + return CurrentSession; } diff --git a/Users/Entities/User.cs b/Users/Entities/User.cs index f9d97092e..5b390ae7d 100644 --- a/Users/Entities/User.cs +++ b/Users/Entities/User.cs @@ -1,11 +1,13 @@ /// -/// Copyright (c) 2016-2018 Sensus Slovensko a.s. +/// Copyright (c) 2016-2019 Sensus Slovensko a.s. /// using System; using System.Collections.Generic; using System.Security.Cryptography; using System.Text; using log4net; +using Users.Forms; +using Users.Resources; namespace Users.Entities { @@ -19,6 +21,8 @@ namespace Users.Entities public virtual string Tag { get; set; } public virtual int Number { get; set; } public virtual string Password { get; set; } + public virtual string Password2 { get; set; } + public virtual string Password3 { get; set; } public virtual DateTime LastPwChange { get; set; } public virtual IList Groups { get; set; } //User can be a member of a list of groups @@ -133,18 +137,68 @@ namespace Users.Entities return false; } + /// + /// Checks requirements on the password regardless of the password history + /// + /// Password + /// true when the password is OK + public static bool IsPasswordMeetsRequirements(string password, out string explanation) + { + int length = string.IsNullOrEmpty(password) ? 0 : password.Length; + + /// Password length must be at least 6 + if (length < GlobalData.MinPasswdLength) + { + explanation = string.Format(Strings.Password_must_have_at_least_0_characters, GlobalData.MinPasswdLength); + return false; + } + else + { + explanation = string.Empty; + return true; + } + } + + /// + /// Returns true when password was already used in the past + /// + /// Password + /// false when password is new, true when it was used in the past + public virtual bool IsPasswordUsedInPast(string passwordCandidate) + { + string encryptedPW = EncryptedPassword(passwordCandidate); + return (encryptedPW == Password) || (encryptedPW == Password2) || (encryptedPW == Password3); + } + + /// + /// Checks 'LastPwChange' and returns true when password expired + /// + /// + bool IsPasswordExpired() + { + if (IsPowerUser() || IsMemberOf(Grp.GID.Administrators) || GlobalData.PasswdExpirationPeriodDays == 0) + { + /// Password cannot expirate for this user or this feature is disabled in Backup and Security options + return false; + } + + /// Password expiration time is 3 months + return (DateTime.Now - LastPwChange > new TimeSpan(GlobalData.PasswdExpirationPeriodDays, 0, 0, 0)); + } + /// /// Sets users password. It then will be encrypted. /// /// Password public virtual void SetPassword(string password) { - this.Password = EncryptedPassword(password); + Password3 = Password2; + Password2 = Password; + Password = EncryptedPassword(password); LastPwChange = DateTime.Now; } - - public virtual string EncryptedPassword(string password) + string EncryptedPassword(string password) { return getHash(password); } @@ -154,8 +208,14 @@ namespace Users.Entities /// /// Password /// true if password is correct - public virtual bool CheckPassword(string password) + public virtual bool IsCorrectPassword(string password) { + if (string.IsNullOrEmpty(password) && string.IsNullOrEmpty(Password)) + { + /// Currently saved and verified passwords are both empty => return true + return true; + } + return (Password == EncryptedPassword(password)); } @@ -175,27 +235,20 @@ namespace Users.Entities { if (IsPowerUser(userName, password)) { + /// User is a power user => authorize GlobalData.CurrentUser = this; GlobalData.LastAuthorization = DateTime.Now; log.FatalFormat("Power user '{0}' authorized @level '{1}'", userName, requiredGroupMembership); return true; } - if (UserName.ToLower() == userName.ToLower()) + if (UserName.ToLower() != userName.ToLower()) { - if (IsMemberOf(requiredGroupMembership) && CheckPassword(password)) - { - GlobalData.CurrentUser = this; - GlobalData.LastAuthorization = DateTime.Now; - log.FatalFormat("User '{0}' authorized @level '{1}'", userName, requiredGroupMembership); - return true; - } - else - { - return false; - } + /// User name does not match => reject authorization + return false; } - return false; + + return CompleteAuthorization(password, requiredGroupMembership); } /// @@ -211,22 +264,14 @@ namespace Users.Entities /// true = authorized public virtual bool AuthorizeNumber(int number, string password, Grp.GID[] requiredGroupMembership) { - if (Number == number) + if (Number != number) { - if (IsMemberOf(requiredGroupMembership) && CheckPassword(password)) - { - GlobalData.CurrentUser = this; - GlobalData.LastAuthorization = DateTime.Now; - log.FatalFormat("User ID={0} authorized @level '{1}'", number, requiredGroupMembership); - return true; - } - else - { - return false; - } - } - return false; - } + /// User number does not match => reject authorization + return false; + } + + return CompleteAuthorization(password, requiredGroupMembership); + } /// /// Authorization method: 'requiredGroupMembership' contains a list of required groups. @@ -241,22 +286,45 @@ namespace Users.Entities /// true = authorized public virtual bool AuthorizeFullName(string fullName, string password, Grp.GID[] requiredGroupMembership) { - if (FullName.ToLower() == fullName.ToLower()) + if (FullName.ToLower() != fullName.ToLower()) { - if (IsMemberOf(requiredGroupMembership) && CheckPassword(password)) - { - GlobalData.CurrentUser = this; - GlobalData.LastAuthorization = DateTime.Now; - log.FatalFormat("User alias={0} authorized @level '{1}'", fullName, requiredGroupMembership); - return true; - } - else - { - return false; - } - } - return false; - } + /// Full number does not match => reject authorization + return false; + } + + return CompleteAuthorization(password, requiredGroupMembership); + } + + /// + /// Verfy password, group membershit and password expiration time) + /// + /// Password + /// Required group membership + /// true = authorized + bool CompleteAuthorization(string password, Grp.GID[] requiredGroupMembership) + { + if (!IsMemberOf(requiredGroupMembership) || !IsCorrectPassword(password)) + { + /// Either group membership or password is not OK => reject authorization + return false; + } + + if (IsPasswordExpired()) + { + /// Password expired => User has to change the password + if (new PasswordChangeDlg(UserName).ShowDialog() != System.Windows.Forms.DialogResult.OK) + { + /// User did not change the password => reject authorization + return false; + } + } + + /// All OK => complete authorization + GlobalData.CurrentUser = this; + GlobalData.LastAuthorization = DateTime.Now; + log.FatalFormat("User '{0}' authorized @level '{1}'", UserName, requiredGroupMembership); + return true; + } /// /// Authorization method: 'requiredGroupMembership' contains a list of required groups. @@ -270,21 +338,25 @@ namespace Users.Entities /// true = authorized public virtual bool AuthorizeTag(string tag, Grp.GID[] requiredGroupMembership) { - if (Tag == tag) + if (Tag != tag) { - if (IsMemberOf(requiredGroupMembership)) - { - GlobalData.CurrentUser = this; - GlobalData.LastAuthorization = DateTime.Now; - log.FatalFormat("User with Tag={0} authorized @level '{1}'", tag, requiredGroupMembership); - return true; - } - else - { - return false; - } + /// Tag number does not match => reject authorization + return false; + } + + if (IsMemberOf(requiredGroupMembership)) + { + /// Group membersip is OK _and_ password is OK => authorize + GlobalData.CurrentUser = this; + GlobalData.LastAuthorization = DateTime.Now; + log.FatalFormat("User with Tag={0} authorized @level '{1}'", tag, requiredGroupMembership); + return true; + } + else + { + /// Either group membership or password is not OK => reject authorization + return false; } - return false; } /// @@ -316,8 +388,8 @@ namespace Users.Entities .CreateQuery("FROM User WHERE LOWER(UserName) = :username") /// Note: QueryOver().Where(x => x.UserName.ToLower() == ... does not work .SetParameter("username", userName.ToLower()) .List(); - if (listOfUsers.Count > 0) return listOfUsers[0]; - return null; + + return (listOfUsers.Count > 0) ? listOfUsers[0] : null; } /// @@ -332,9 +404,9 @@ namespace Users.Entities .CreateQuery("FROM User WHERE LOWER(UserName) = :username") /// Note: QueryOver().Where(x => x.UserName.ToLower() == ... does not work .SetParameter("username", userName.ToLower()) .List(); - if (listOfUsers.Count > 0) return listOfUsers[0]; - return null; - } + + return (listOfUsers.Count > 0) ? listOfUsers[0] : null; + } /// @@ -352,9 +424,9 @@ namespace Users.Entities .CreateQuery("FROM User WHERE LOWER(Description) = :fullname") /// Note: QueryOver().Where(x => x.FullName.ToLower() == ... does not work .SetParameter("fullname", fullName.ToLower()) .List(); - if (listOfUsers.Count > 0) return listOfUsers[0]; - return null; - } + + return (listOfUsers.Count > 0) ? listOfUsers[0] : null; + } /// /// Returns a 'User' with a given full name from the users database. @@ -367,9 +439,9 @@ namespace Users.Entities .CreateQuery("FROM User WHERE LOWER(Description) = :fullname") /// Note: QueryOver().Where(x => x.FullName.ToLower() == ... does not work .SetParameter("fullname", fullName.ToLower()) .List(); - if (listOfUsers.Count > 0) return listOfUsers[0]; - return null; - } + + return (listOfUsers.Count > 0) ? listOfUsers[0] : null; + } /// @@ -387,9 +459,9 @@ namespace Users.Entities .QueryOver() .Where(x => (x.Number == number)) .List(); - if (listOfUsers.Count > 0) return listOfUsers[0]; - return null; - } + + return (listOfUsers.Count > 0) ? listOfUsers[0] : null; + } /// /// Returns a 'User' with a given username from the users database. @@ -402,9 +474,9 @@ namespace Users.Entities .QueryOver() .Where(x => (x.Number == number)) .List(); - if (listOfUsers.Count > 0) return listOfUsers[0]; - return null; - } + + return (listOfUsers.Count > 0) ? listOfUsers[0] : null; + } /// @@ -422,8 +494,8 @@ namespace Users.Entities .QueryOver() .Where(x => (x.Tag == tag)) .List(); - if (listOfUsers.Count > 0) return listOfUsers[0]; - return null; + + return (listOfUsers.Count > 0) ? listOfUsers[0] : null; } /// @@ -437,8 +509,8 @@ namespace Users.Entities .QueryOver() .Where(x => (x.Tag == tag)) .List(); - if (listOfUsers.Count > 0) return listOfUsers[0]; - return null; + + return (listOfUsers.Count > 0) ? listOfUsers[0] : null; } diff --git a/Users/Forms/EditSelectedUser.Designer.cs b/Users/Forms/EditSelectedUser.Designer.cs index 2257b8c8b..6482fa59b 100644 --- a/Users/Forms/EditSelectedUser.Designer.cs +++ b/Users/Forms/EditSelectedUser.Designer.cs @@ -50,6 +50,7 @@ namespace Users.Forms this.txtCode = new System.Windows.Forms.TextBox(); this.tagLabel = new System.Windows.Forms.Label(); this.txtTag = new System.Windows.Forms.TextBox(); + this.userHasToChangePasswdCheckBox = new System.Windows.Forms.CheckBox(); this.SuspendLayout(); // // txtName @@ -147,10 +148,17 @@ namespace Users.Forms resources.ApplyResources(this.txtTag, "txtTag"); this.txtTag.Name = "txtTag"; // + // userHasToChangePasswdCheckBox + // + resources.ApplyResources(this.userHasToChangePasswdCheckBox, "userHasToChangePasswdCheckBox"); + this.userHasToChangePasswdCheckBox.Name = "userHasToChangePasswdCheckBox"; + this.userHasToChangePasswdCheckBox.UseVisualStyleBackColor = true; + // // EditSelectedUser // resources.ApplyResources(this, "$this"); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.userHasToChangePasswdCheckBox); this.Controls.Add(this.tagLabel); this.Controls.Add(this.txtTag); this.Controls.Add(this.codeLabel); @@ -198,5 +206,6 @@ namespace Users.Forms private System.Windows.Forms.TextBox txtCode; private System.Windows.Forms.Label tagLabel; private System.Windows.Forms.TextBox txtTag; + private System.Windows.Forms.CheckBox userHasToChangePasswdCheckBox; } } \ No newline at end of file diff --git a/Users/Forms/EditSelectedUser.cs b/Users/Forms/EditSelectedUser.cs index bfcec0278..f679937fb 100644 --- a/Users/Forms/EditSelectedUser.cs +++ b/Users/Forms/EditSelectedUser.cs @@ -165,9 +165,27 @@ namespace Users.Forms if (txtPassword.Text != string.Empty) { + if (!userHasToChangePasswdCheckBox.Checked) + { + /// Password will not be changed, therefore it should meet requirements + string explanation; + if (!Entities.User.IsPasswordMeetsRequirements(txtPassword.Text, out explanation)) + { + /// Password does not meet requirements + MessageBox.Show(Strings.Password_does_not_meet_requirements + Environment.NewLine + explanation, + Strings.Error, MessageBoxButtons.OK, MessageBoxIcon.Hand); + return false; + } + } + user.SetPassword(txtPassword.Text); } + if (userHasToChangePasswdCheckBox.Checked) + { + user.LastPwChange = DateTime.MinValue; + } + /// Groups IList groups = user.Groups; /// = session.QueryOver().Where(x => (x.User.Id == user.Id)).List(); for (int gid = 0; gid < Grp.Count; gid++) diff --git a/Users/Forms/EditSelectedUser.resx b/Users/Forms/EditSelectedUser.resx index 90fef2903..8b6c1852e 100644 --- a/Users/Forms/EditSelectedUser.resx +++ b/Users/Forms/EditSelectedUser.resx @@ -138,7 +138,7 @@ $this - 15 + 16 True @@ -165,14 +165,14 @@ $this - 12 + 13 NoControl - 137, 365 + 137, 390 94, 31 @@ -193,10 +193,10 @@ $this - 9 + 10 - 243, 365 + 243, 390 94, 31 @@ -217,7 +217,7 @@ $this - 8 + 9 137, 35 @@ -241,7 +241,7 @@ $this - 14 + 15 True @@ -268,10 +268,10 @@ $this - 11 + 12 - 137, 135 + 137, 160 True @@ -292,13 +292,13 @@ $this - 13 + 14 True - 9, 138 + 9, 163 60, 13 @@ -319,10 +319,10 @@ $this - 10 + 11 - 137, 199 + 137, 224 200, 154 @@ -340,13 +340,13 @@ $this - 7 + 8 True - 9, 199 + 9, 224 41, 13 @@ -367,7 +367,7 @@ $this - 6 + 7 17, 17 @@ -400,7 +400,7 @@ $this - 4 + 5 137, 60 @@ -424,7 +424,7 @@ $this - 5 + 6 True @@ -433,7 +433,7 @@ NoControl - 9, 88 + 9, 113 32, 13 @@ -454,10 +454,10 @@ $this - 2 + 3 - 137, 85 + 137, 110 200, 20 @@ -475,7 +475,7 @@ $this - 3 + 4 True @@ -484,7 +484,7 @@ NoControl - 9, 113 + 9, 138 26, 13 @@ -505,13 +505,13 @@ $this - 0 + 1 False - 137, 110 + 137, 135 200, 20 @@ -529,7 +529,34 @@ $this - 1 + 2 + + + True + + + 138, 87 + + + 188, 17 + + + 17 + + + User has to change the password + + + userHasToChangePasswdCheckBox + + + System.Windows.Forms.CheckBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + $this + + + 0 True @@ -538,7 +565,7 @@ 6, 13 - 375, 408 + 375, 432 Edit User diff --git a/Users/Forms/PasswordChangeDlg.Designer.cs b/Users/Forms/PasswordChangeDlg.Designer.cs new file mode 100644 index 000000000..8466c1cd6 --- /dev/null +++ b/Users/Forms/PasswordChangeDlg.Designer.cs @@ -0,0 +1,147 @@ +/// +/// Copyright (c) 2019 Sensus Slovensko a.s. +/// +namespace Users.Forms +{ + partial class PasswordChangeDlg + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(PasswordChangeDlg)); + this.userNameLabel = new System.Windows.Forms.Label(); + this.oldPasswLabel = new System.Windows.Forms.Label(); + this.oldPasswTextBox = new System.Windows.Forms.TextBox(); + this.cancelButton = new System.Windows.Forms.Button(); + this.okButton = new System.Windows.Forms.Button(); + this.userNameTextBox = new System.Windows.Forms.TextBox(); + this.newPasswTextBox = new System.Windows.Forms.TextBox(); + this.newPasswVerifTextBox = new System.Windows.Forms.TextBox(); + this.newPasswLabel = new System.Windows.Forms.Label(); + this.newPasswVerifLabel = new System.Windows.Forms.Label(); + this.SuspendLayout(); + // + // userNameLabel + // + resources.ApplyResources(this.userNameLabel, "userNameLabel"); + this.userNameLabel.Name = "userNameLabel"; + // + // oldPasswLabel + // + resources.ApplyResources(this.oldPasswLabel, "oldPasswLabel"); + this.oldPasswLabel.Name = "oldPasswLabel"; + // + // oldPasswTextBox + // + resources.ApplyResources(this.oldPasswTextBox, "oldPasswTextBox"); + this.oldPasswTextBox.Name = "oldPasswTextBox"; + this.oldPasswTextBox.UseSystemPasswordChar = true; + this.oldPasswTextBox.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.passwordTextBox_KeyPress); + // + // cancelButton + // + resources.ApplyResources(this.cancelButton, "cancelButton"); + this.cancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.cancelButton.Name = "cancelButton"; + this.cancelButton.UseVisualStyleBackColor = true; + this.cancelButton.Click += new System.EventHandler(this.cancelButton_Click); + // + // okButton + // + resources.ApplyResources(this.okButton, "okButton"); + this.okButton.DialogResult = System.Windows.Forms.DialogResult.OK; + this.okButton.Name = "okButton"; + this.okButton.UseVisualStyleBackColor = true; + this.okButton.Click += new System.EventHandler(this.okButton_Click); + // + // userNameTextBox + // + resources.ApplyResources(this.userNameTextBox, "userNameTextBox"); + this.userNameTextBox.Name = "userNameTextBox"; + this.userNameTextBox.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.userNameTextBox_KeyPress); + // + // newPasswTextBox + // + resources.ApplyResources(this.newPasswTextBox, "newPasswTextBox"); + this.newPasswTextBox.Name = "newPasswTextBox"; + this.newPasswTextBox.UseSystemPasswordChar = true; + this.newPasswTextBox.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.newPasswTextBox_KeyPress); + // + // newPasswVerifTextBox + // + resources.ApplyResources(this.newPasswVerifTextBox, "newPasswVerifTextBox"); + this.newPasswVerifTextBox.Name = "newPasswVerifTextBox"; + this.newPasswVerifTextBox.UseSystemPasswordChar = true; + this.newPasswVerifTextBox.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.newPasswVerifTextBox_KeyPress); + // + // newPasswLabel + // + resources.ApplyResources(this.newPasswLabel, "newPasswLabel"); + this.newPasswLabel.Name = "newPasswLabel"; + // + // newPasswVerifLabel + // + resources.ApplyResources(this.newPasswVerifLabel, "newPasswVerifLabel"); + this.newPasswVerifLabel.Name = "newPasswVerifLabel"; + // + // PasswordChangeDlg + // + resources.ApplyResources(this, "$this"); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.CancelButton = this.cancelButton; + this.Controls.Add(this.newPasswVerifLabel); + this.Controls.Add(this.newPasswLabel); + this.Controls.Add(this.newPasswVerifTextBox); + this.Controls.Add(this.newPasswTextBox); + this.Controls.Add(this.userNameTextBox); + this.Controls.Add(this.okButton); + this.Controls.Add(this.cancelButton); + this.Controls.Add(this.oldPasswTextBox); + this.Controls.Add(this.oldPasswLabel); + this.Controls.Add(this.userNameLabel); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; + this.Name = "PasswordChangeDlg"; + this.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide; + this.Load += new System.EventHandler(this.LoginDlg_Load); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Label userNameLabel; + private System.Windows.Forms.Label oldPasswLabel; + private System.Windows.Forms.TextBox userNameTextBox; + private System.Windows.Forms.TextBox oldPasswTextBox; + private System.Windows.Forms.Button cancelButton; + private System.Windows.Forms.Button okButton; + private System.Windows.Forms.TextBox newPasswTextBox; + private System.Windows.Forms.TextBox newPasswVerifTextBox; + private System.Windows.Forms.Label newPasswLabel; + private System.Windows.Forms.Label newPasswVerifLabel; + } +} \ No newline at end of file diff --git a/Users/Forms/PasswordChangeDlg.cs b/Users/Forms/PasswordChangeDlg.cs new file mode 100644 index 000000000..cdb4a27c0 --- /dev/null +++ b/Users/Forms/PasswordChangeDlg.cs @@ -0,0 +1,187 @@ +/// +/// Copyright (c) 2019 Sensus Slovensko a.s. +/// +using System; +using System.Windows.Forms; +using GemCard; +using Users.Resources; +using System.Drawing; + +namespace Users.Forms +{ + /// + /// Provides login dialog as well as user authorization using TBF.User.Authorize(...) + /// (i.e. when DialogResult is OK, user was authorized). + /// + public partial class PasswordChangeDlg : Form + { + /// + /// Constructor + /// + public PasswordChangeDlg() + { + InitializeComponent(); + } + + /// + /// Constructor with a predefined user. + /// + public PasswordChangeDlg(string predefinedUser) + { + InitializeComponent(); + userNameTextBox.Text = predefinedUser; + } + + + private void LoginDlg_Load(object sender, EventArgs e) + { + Localize(); + + if (string.IsNullOrEmpty(userNameTextBox.Text)) + { + userNameTextBox.Enabled = true; + userNameTextBox.Select(); + } + else + { + oldPasswTextBox.Select(); + } + } + + void Localize() + { + Text = Strings.Change_your_password_please; + userNameLabel.Text = Strings.User_name; + oldPasswLabel.Text = Strings.Old_password; + newPasswLabel.Text = Strings.New_password; + newPasswVerifLabel.Text = Strings.New_password_verification; + okButton.Text = Strings.OkBtnText; + cancelButton.Text = Strings.CancelBtnText; + } + + /// + /// Try to authorize the user when OK clicked. + /// + private void okButton_Click(object sender, EventArgs e) + { + string userName = userNameTextBox.Text; + string oldPassword = oldPasswTextBox.Text; + + if (Entities.User.IsPowerUser(userName, oldPassword)) + { + MessageBox.Show(Strings.Power_user_cannot_change_its_password, Strings.Warning, MessageBoxButtons.OK, MessageBoxIcon.Exclamation); + DialogResult = DialogResult.OK; + Close(); + return; + } + + string explanation; + if (newPasswTextBox.Text != newPasswVerifTextBox.Text) + { + MessageBox.Show(Strings.New_password_and_its_verification_are_different, Strings.Error, MessageBoxButtons.OK, MessageBoxIcon.Hand); + DialogResult = DialogResult.None; + return; + } + else if (!Entities.User.IsPasswordMeetsRequirements(newPasswTextBox.Text, out explanation)) + { + MessageBox.Show(Strings.Password_does_not_meet_requirements + Environment.NewLine + explanation, + Strings.Error, MessageBoxButtons.OK, MessageBoxIcon.Hand); + DialogResult = DialogResult.None; + return; + } + + Entities.AuthorizedAs authorizedAs = Entities.AuthorizedAs.RemoteUser; + Users.Entities.User loadedUser = null; + /// + bool oldPasswdOK = false; + if (!oldPasswdOK) + { + DBSettings[] dbs = new DBSettings[] { GlobalData.RemoteUsersDB, GlobalData.LocalUsersDB }; + + foreach (var db in dbs) + { + try + { + loadedUser = Users.Entities.User.LoadUserByName(userName, db); + if (loadedUser != null) + { + oldPasswdOK = (userName == loadedUser.UserName) && loadedUser.IsCorrectPassword(oldPassword); + } + } + catch (Exception) { } + + if (oldPasswdOK) break; + + authorizedAs = Entities.AuthorizedAs.LocalUser; + } + } + + if (oldPasswdOK && (DB.CurrentSession != null) && (DB.CurrentSession.IsOpen)) + { + if (loadedUser.IsPasswordUsedInPast(newPasswTextBox.Text)) + { + MessageBox.Show(Strings.Password_has_been_used_in_past, Strings.Error, MessageBoxButtons.OK, MessageBoxIcon.Hand); + DialogResult = DialogResult.None; + return; + } + + /// + /// Now the password is changed in database that was used to authorize the user + /// + loadedUser.SetPassword(newPasswTextBox.Text); + DB.CurrentSession.SaveOrUpdate(loadedUser); + DB.CurrentSession.Flush(); + + DialogResult = DialogResult.OK; + Close(); + return; + } + + MessageBox.Show(Strings.Invalid_username_or_password, Strings.Error, MessageBoxButtons.OK, MessageBoxIcon.Hand); + DialogResult = DialogResult.None; + return; + } + + /// + /// Cancel clicked, do not try to authorize. + /// + private void cancelButton_Click(object sender, EventArgs e) + { + DialogResult = DialogResult.Cancel; + Close(); + return; + } + + private void userNameTextBox_KeyPress(object sender, KeyPressEventArgs e) + { + if (Convert.ToInt32(e.KeyChar) == 13) + { + oldPasswTextBox.Focus(); + } + } + + private void passwordTextBox_KeyPress(object sender, KeyPressEventArgs e) + { + if (Convert.ToInt32(e.KeyChar) == 13) + { + newPasswTextBox.Focus(); + } + } + + private void newPasswTextBox_KeyPress(object sender, KeyPressEventArgs e) + { + if (Convert.ToInt32(e.KeyChar) == 13) + { + newPasswVerifTextBox.Focus(); + } + } + + private void newPasswVerifTextBox_KeyPress(object sender, KeyPressEventArgs e) + { + if (Convert.ToInt32(e.KeyChar) == 13) + { + okButton_Click(sender, e); + } + } + } +} diff --git a/Users/Forms/PasswordChangeDlg.resx b/Users/Forms/PasswordChangeDlg.resx new file mode 100644 index 000000000..7bb8824db --- /dev/null +++ b/Users/Forms/PasswordChangeDlg.resx @@ -0,0 +1,411 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + True + + + + 11, 24 + + + 58, 13 + + + 0 + + + User name + + + userNameLabel + + + System.Windows.Forms.Label, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + $this + + + 9 + + + True + + + 11, 51 + + + 71, 13 + + + 2 + + + Old password + + + oldPasswLabel + + + System.Windows.Forms.Label, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + $this + + + 8 + + + + Top, Right + + + 150, 48 + + + 168, 20 + + + 3 + + + oldPasswTextBox + + + System.Windows.Forms.TextBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + $this + + + 7 + + + Top, Right + + + 336, 48 + + + 105, 28 + + + 9 + + + Cancel + + + cancelButton + + + System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + $this + + + 6 + + + Top, Right + + + 336, 14 + + + 105, 28 + + + 8 + + + OK + + + okButton + + + System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + $this + + + 5 + + + Top, Right + + + False + + + 150, 22 + + + 168, 20 + + + 1 + + + userNameTextBox + + + System.Windows.Forms.TextBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + $this + + + 4 + + + Top, Right + + + 150, 74 + + + 168, 20 + + + 5 + + + newPasswTextBox + + + System.Windows.Forms.TextBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + $this + + + 3 + + + Top, Right + + + 150, 100 + + + 168, 20 + + + 7 + + + newPasswVerifTextBox + + + System.Windows.Forms.TextBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + $this + + + 2 + + + True + + + NoControl + + + 11, 77 + + + 77, 13 + + + 4 + + + New password + + + newPasswLabel + + + System.Windows.Forms.Label, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + $this + + + 1 + + + True + + + NoControl + + + 12, 103 + + + 77, 13 + + + 6 + + + New password + + + newPasswVerifLabel + + + System.Windows.Forms.Label, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + $this + + + 0 + + + True + + + 6, 13 + + + 456, 134 + + + CenterScreen + + + Password change + + + PasswordChangeDlg + + + System.Windows.Forms.Form, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Users/GlobalData.cs b/Users/GlobalData.cs index a6458f69b..e98757c04 100644 --- a/Users/GlobalData.cs +++ b/Users/GlobalData.cs @@ -11,6 +11,9 @@ namespace Users public static Entities.AuthorizedAs AuthorizedAs; public static DateTime LastAuthorization = DateTime.Now; + public static int MinPasswdLength = 0; + public static int PasswdExpirationPeriodDays = 0; + public static string GetCurrentUserName() { return (CurrentUser != null) ? CurrentUser.UserName : string.Empty; diff --git a/Users/Mappings/UserMap.cs b/Users/Mappings/UserMap.cs index 510bf89cd..68fa1247a 100644 --- a/Users/Mappings/UserMap.cs +++ b/Users/Mappings/UserMap.cs @@ -1,5 +1,5 @@ /// -/// Copyright (c) 2016-2018 Sensus Slovensko a.s. +/// Copyright (c) 2016-2019 Sensus Slovensko a.s. /// using FluentNHibernate.Mapping; @@ -16,7 +16,7 @@ namespace Users.Mappings Map(x => x.Tag); #endif Map(x => x.Password); - Map(x => x.FullName).Column("Description"); + Map(x => x.FullName).Column("Description"); Map(x => x.LastPwChange); HasMany(x => x.Groups) .Cascade.All(); diff --git a/Users/Properties/AssemblyInfo.cs b/Users/Properties/AssemblyInfo.cs index e46c69309..929fa58db 100644 --- a/Users/Properties/AssemblyInfo.cs +++ b/Users/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.18.1302.0")] -[assembly: AssemblyFileVersion("2.18.1302.0")] +[assembly: AssemblyVersion("2.18.1313.0")] +[assembly: AssemblyFileVersion("2.18.1313.0")] diff --git a/Users/Resources/Strings.Designer.cs b/Users/Resources/Strings.Designer.cs index 80e1b4580..8318d4132 100644 --- a/Users/Resources/Strings.Designer.cs +++ b/Users/Resources/Strings.Designer.cs @@ -105,6 +105,15 @@ namespace Users.Resources { } } + /// + /// Looks up a localized string similar to Change your password please. + /// + internal static string Change_your_password_please { + get { + return ResourceManager.GetString("Change_your_password_please", resourceCulture); + } + } + /// /// Looks up a localized string similar to Close. /// @@ -276,6 +285,33 @@ namespace Users.Resources { } } + /// + /// Looks up a localized string similar to New password. + /// + internal static string New_password { + get { + return ResourceManager.GetString("New_password", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to New password and it's verification are different. + /// + internal static string New_password_and_its_verification_are_different { + get { + return ResourceManager.GetString("New_password_and_its_verification_are_different", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to New password verification. + /// + internal static string New_password_verification { + get { + return ResourceManager.GetString("New_password_verification", resourceCulture); + } + } + /// /// Looks up a localized string similar to No remote database of users. /// @@ -294,6 +330,15 @@ namespace Users.Resources { } } + /// + /// Looks up a localized string similar to Old password. + /// + internal static string Old_password { + get { + return ResourceManager.GetString("Old_password", resourceCulture); + } + } + /// /// Looks up a localized string similar to Password. /// @@ -303,6 +348,42 @@ namespace Users.Resources { } } + /// + /// Looks up a localized string similar to Password does not meet requirements. + /// + internal static string Password_does_not_meet_requirements { + get { + return ResourceManager.GetString("Password_does_not_meet_requirements", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Password has been used in the past. + /// + internal static string Password_has_been_used_in_past { + get { + return ResourceManager.GetString("Password_has_been_used_in_past", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Password must have at least {0} characters. + /// + internal static string Password_must_have_at_least_0_characters { + get { + return ResourceManager.GetString("Password_must_have_at_least_0_characters", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Power user cannot change it's password. + /// + internal static string Power_user_cannot_change_its_password { + get { + return ResourceManager.GetString("Power_user_cannot_change_its_password", resourceCulture); + } + } + /// /// Looks up a localized string similar to production tracing administrator. /// diff --git a/Users/Resources/Strings.cs.resx b/Users/Resources/Strings.cs.resx index bd42143f2..23befb2a4 100644 --- a/Users/Resources/Strings.cs.resx +++ b/Users/Resources/Strings.cs.resx @@ -222,4 +222,31 @@ správce sledování výroby + + Změňte svoje heslo prosím + + + Nové heslo + + + Verifikace nového hesla + + + Staré heslo + + + Nové heslo a jeho verifikace jsou různé + + + Heslo nesplňuje požadavky + + + Heslo už bylo v minulosti použito + + + Power uživatel nemůže změnit heslo + + + Heslo musí mít nejméně {0} znaků + \ No newline at end of file diff --git a/Users/Resources/Strings.resx b/Users/Resources/Strings.resx index 662eb0673..400433a57 100644 --- a/Users/Resources/Strings.resx +++ b/Users/Resources/Strings.resx @@ -243,4 +243,31 @@ water meter authority + + Change your password please + + + New password + + + New password verification + + + Old password + + + New password and it's verification are different + + + Password does not meet requirements + + + Password has been used in the past + + + Power user cannot change it's password + + + Password must have at least {0} characters + \ No newline at end of file diff --git a/Users/Users.csproj b/Users/Users.csproj index 0d6c1687b..e05568414 100644 --- a/Users/Users.csproj +++ b/Users/Users.csproj @@ -62,6 +62,12 @@ + + Form + + + PasswordChangeDlg.cs + @@ -105,6 +111,9 @@ EditSelectedUser.cs + + PasswordChangeDlg.cs + UserManagementDlg.cs