//******************************************************************************************************
// 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
}
}