//****************************************************************************************************** // SecurityProviderBase.cs - Gbtc // // Copyright © 2012, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the MIT License (MIT), the "License"; you may // not use this file except in compliance with the License. You may obtain a copy of the License at: // // http://www.opensource.org/licenses/MIT // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 03/22/2010 - Pinal C. Patel // Generated original version of source code. // 05/24/2010 - Pinal C. Patel // Modified RefreshData() method to not query the AD at all for external user. // 05/27/2010 - Pinal C. Patel // Added usage example to code comments. // 06/15/2010 - Pinal C. Patel // Added checks to the in-process caching logic. // 06/24/2010 - Pinal C. Patel // Added LogError() and LogLogin() methods. // 06/25/2010 - Pinal C. Patel // Fixed a issue in the caching mechanism employed in Current static property. // 12/03/2010 - Pinal C. Patel // Added TranslateRole() to allow providers to perform translation on role name. // 01/05/2011 - Pinal C. Patel // Added CanRefreshData, CanUpdateData, CanResetPassword and CanChangePassword properties along // with accompanying RefreshData(), UpdateData(), ResetPassword() and ChangePassword() methods. // 11/23/2011 - J. Ritchie Carroll // Modified to support buffer optimized ISupportBinaryImage. // 12/20/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.Configuration; using System.Diagnostics; using System.Security; using System.Security.Principal; using GSF.Configuration; namespace GSF.Security { /// /// Base class for a provider of role-based security in applications. /// /// /// This examples shows how to extend to use a flat-file for the security data store: /// /// using System.Data; /// using System.IO; /// using GSF; /// using GSF.Data; /// using GSF.IO; /// using GSF.Security; /// /// namespace CustomSecurity /// { /// public class FlatFileSecurityProvider : SecurityProviderBase /// { /// private const int LeastPrivilegedLevel = 5; /// /// public FlatFileSecurityProvider(string username) /// : base(username) /// { /// } /// /// public override bool RefreshData() /// { /// // Check for a valid username. /// if (string.IsNullOrEmpty(UserData.Username)) /// return false; /// /// // Check if a file name is specified. /// if (string.IsNullOrEmpty(ConnectionString)) /// return false; /// /// // Check if file exist on file system. /// string file = FilePath.GetAbsolutePath(ConnectionString); /// if (!File.Exists(file)) /// return false; /// /// // Read the data from the specified file. /// DataTable data = File.ReadAllText(file).ToDataTable(",", true); /// DataRow[] user = data.Select(string.Format("Username = '{0}'", UserData.Username)); /// if (user.Length > 0) /// { /// // User exists in the specified file. /// UserData.IsDefined = true; /// UserData.Password = user[0]["Password"].ToNonNullString(); /// /// for (int i = LeastPrivilegedLevel; i >= int.Parse(user[0]["Level"].ToNonNullString()); i--) /// { /// UserData.Roles.Add(i.ToString()); /// } /// } /// /// return true; /// } /// /// public override bool Authenticate(string password) /// { /// // Compare password hashes to authenticate. /// return (UserData.Password == SecurityProviderUtility.EncryptPassword(password)); /// } /// } /// } /// /// Config file entries that go along with the above example: /// /// /// /// ///
/// /// /// /// /// /// /// /// /// /// /// /// /// /// ]]> /// /// /// /// public abstract class SecurityProviderBase : ISecurityProvider { #region [ Members ] // Constants /// /// Specifies the default value for the property. /// public const string DefaultApplicationName = "SEC_APP"; /// /// Specifies the default value for the property. /// public const string DefaultConnectionString = "Primary={Server=DB1;Database=AppSec;Trusted_Connection=True};Backup={Server=DB2;Database=AppSec;Trusted_Connection=True}"; /// /// Specifies the default value for the property. /// public const bool DefaultPersistSettings = true; /// /// Specifies the default value for the property. /// public const string DefaultSettingsCategory = "SecurityProvider"; // Fields private string m_applicationName; private string m_connectionString; private IPrincipal m_passthroughPrincipal; private SecureString m_securePassword; private string m_authenticationFailureReason; private string m_settingsCategory; private UserData m_userData; private bool m_isUserAuthenticated; private bool m_persistSettings; private readonly bool m_canRefreshData; private readonly bool m_canResetPassword; private readonly bool m_canChangePassword; private LogEventFunctionSignature m_logEvent; #endregion #region [ Constructors ] /// /// Initializes a new instance of the security provider. /// /// Name that uniquely identifies the user. /// true if the security provider can refresh from the backend data store, otherwise false. /// true if the security provider can reset user password, otherwise false. /// true if the security provider can change user password, otherwise false. protected SecurityProviderBase(string username, bool canRefreshData, bool canResetPassword, bool canChangePassword) { // Initialize member variables. m_userData = new UserData(username); m_canRefreshData = canRefreshData; m_canResetPassword = canResetPassword; m_canChangePassword = canChangePassword; m_applicationName = DefaultApplicationName; m_connectionString = DefaultConnectionString; m_persistSettings = DefaultPersistSettings; m_settingsCategory = DefaultSettingsCategory; m_logEvent = EventLog.WriteEntry; } #endregion #region [ Properties ] /// /// Gets or sets the name of the application being secured as defined in the backend security datastore. /// public string ApplicationName { get => m_applicationName; set => m_applicationName = value; } /// /// Gets or sets the connection string to be used for connection to the backend security datastore. /// public string ConnectionString { get => m_connectionString; set => m_connectionString = value; } /// /// Gets or sets the principal used for passthrough authentication. /// public IPrincipal PassthroughPrincipal { get => m_passthroughPrincipal; set => m_passthroughPrincipal = value; } /// /// Gets or sets the password as a . /// public virtual SecureString SecurePassword { get => m_securePassword; set { if (m_securePassword is not null) m_securePassword.Dispose(); m_securePassword = value; } } /// /// Gets or sets as clear text password. /// public string Password { get => SecurePassword.ToUnsecureString(); set => SecurePassword = value.ToSecureString(); } /// /// Gets or sets the to use for logging security events for the implementation. /// /// /// Set to null to use default handler, i.e., . /// public virtual LogEventFunctionSignature LogEvent { get => m_logEvent; set => m_logEvent = value is null ? EventLog.WriteEntry : value; } /// /// Gets or sets a boolean value that indicates whether security provider settings are to be saved to the config file. /// public bool PersistSettings { get => m_persistSettings; set => m_persistSettings = value; } /// /// Gets or sets the category under which security provider settings are to be saved to the config file if the property is set to true. /// /// The value being assigned is a null or empty string. public string SettingsCategory { get => m_settingsCategory; set { if (string.IsNullOrEmpty(value)) throw new ArgumentNullException(nameof(value)); m_settingsCategory = value; } } /// /// Gets the object containing information about the user. /// public virtual UserData UserData { get => m_userData; protected set => m_userData = value; } /// /// Gets the flag that indicates whether the user was /// authenticated during the last authentication attempt. /// public virtual bool IsUserAuthenticated { get => m_isUserAuthenticated; protected set => m_isUserAuthenticated = value; } /// /// Gets a boolean value that indicates whether operation is supported. /// public virtual bool CanRefreshData => m_canRefreshData; /// /// Gets a boolean value that indicates whether operation is supported. /// public virtual bool CanResetPassword => m_canResetPassword; /// /// Gets a boolean value that indicates whether operation is supported. /// public virtual bool CanChangePassword => m_canChangePassword; /// /// Gets or allows derived classes to set an authentication failure reason. /// public virtual string AuthenticationFailureReason { get => m_authenticationFailureReason; protected set => m_authenticationFailureReason = value; } /// /// Gets the flag that indicates whether the user /// needs to be redirected after the Authentication attempt. /// public virtual bool IsRedirectRequested => false; /// /// Gets the URI that user will be redirected to if is set. /// public virtual string RequestedRedirect => ""; #endregion #region [ Methods ] #region [ Abstract ] /// /// When overridden in a derived class, authenticates the user. /// /// true if the user is authenticated, otherwise false. public abstract bool Authenticate(); /// /// When overridden in a derived class, refreshes the from the backend datastore. /// /// true if is refreshed, otherwise false. public abstract bool RefreshData(); /// /// When overridden in a derived class, resets user password in the backend datastore. /// /// Answer to the user's security question. /// true if the password is reset, otherwise false. public abstract bool ResetPassword(string securityAnswer); /// /// When overridden in a derived class, changes user password in the backend datastore. /// /// User's current password. /// User's new password. /// true if the password is changed, otherwise false. public abstract bool ChangePassword(string oldPassword, string newPassword); #endregion /// /// Saves security provider settings to the config file if the property is set to true. /// /// has a value of null or empty string. public virtual void SaveSettings() { if (!m_persistSettings) return; // Ensure that settings category is specified. if (string.IsNullOrEmpty(m_settingsCategory)) throw new ConfigurationErrorsException("SettingsCategory property has not been set"); // Save settings under the specified category. ConfigurationFile config = ConfigurationFile.Current; CategorizedSettingsElementCollection settings = config.Settings[m_settingsCategory]; settings["ApplicationName", true].Update(m_applicationName); settings["ConnectionString", true].Update(m_connectionString); config.Save(); } /// /// Loads saved security provider settings from the config file if the property is set to true. /// /// has a value of null or empty string. public virtual void LoadSettings() { if (!m_persistSettings) return; // Ensure that settings category is specified. if (string.IsNullOrEmpty(m_settingsCategory)) throw new ConfigurationErrorsException("SettingsCategory property has not been set"); // Load settings from the specified category. ConfigurationFile config = ConfigurationFile.Current; CategorizedSettingsElementCollection settings = config.Settings[m_settingsCategory]; settings.Add("ApplicationName", m_applicationName, "Name of the application being secured as defined in the backend security data store."); settings.Add("ConnectionString", m_connectionString, "Connection string to be used for connection to the backend security data store."); ApplicationName = settings["ApplicationName"].ValueAs(m_applicationName); ConnectionString = settings["ConnectionString"].ValueAs(m_connectionString); } /// /// Performs a translation of the specified user . /// /// The user role to be translated. /// The user role that the specified user translates to. public virtual string TranslateRole(string role) => role; // Most providers will not perform any kind of translation on role names. /// /// Performs a translation of the default login page to a different endpoint. /// /// The URI of the login page specified in the AppSettings /// The URI originally requested. /// The URI requested by the client /// The Referrer as specified in the request header /// The URI to be redirected to public virtual string TranslateRedirect(string loginUrl, Uri uri, string encodedPath, string referrer) => $"{loginUrl}?redir={encodedPath}{referrer}"; /// /// Gets a list of Roles for this user for a specified ApplicationId. /// /// The applicationId for the roles to be returned. /// The roles that the specified user has. public virtual List GetUserRoles(string applicationId) => UserData?.Roles ?? new List(); #endregion } }