//******************************************************************************************************
// LdapSecurityProvider.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:
// ----------------------------------------------------------------------------------------------------
// 07/08/2010 - Pinal C. Patel
// Generated original version of source code.
// 12/03/2010 - Pinal C. Patel
// Override the default behavior of TranslateRole() to translate a SID to its role name.
// 01/05/2011 - Pinal C. Patel
// Added overrides to RefreshData(), UpdateData(), ResetPassword() and ChangePassword() methods.
// 02/14/2011 - J. Ritchie Carroll
// Modified provider to be able to use local accounts when user is not connected to a domain.
// 06/09/2011 - Pinal C. Patel
// Fixed a issue in the caching logic of RefreshData() method.
// 08/16/2011 - Pinal C. Patel
// Made offline caching of user data for authentication purpose optional and turned on by default.
// 12/20/2012 - Starlynn Danyelle Gilliam
// Modified Header.
// 03/08/2013 - Pinal C. Patel
// Modified to enabled persistence on UserInfo only temporarily prior to calling Initialize() to
// load privileged user credentials if specified. This is to prevent any accidental updates to the
// config file when object gets disposed which would cause a web application to restart.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Security;
using System.Security.Principal;
using GSF.Configuration;
using GSF.Identity;
namespace GSF.Security
{
///
/// Represents an that uses Active Directory for its backend data store and credential authentication.
///
///
/// A Security Identifier can also be specified in
/// IncludedResources instead of a role name in the format of 'SID:<Security Identifier>' (Example: SID:S-1-5-21-19610888-1443184010-1631745340-269783).
///
///
/// Required config file entries:
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
/// ]]>
///
///
public class LdapSecurityProvider : SecurityProviderBase
{
#region [ Members ]
// Constants
///
/// Defines the provider ID for the .
///
public const int ProviderID = 0;
///
/// Specifies the default value for the property.
///
public const bool DefaultEnableOfflineCaching = true;
///
/// Specifies the default value for the property.
///
public const double DefaultCacheRetryDelayInterval = 1000.0D;
///
/// Specifies the default value for the property.
///
public const int DefaultCacheMaximumRetryAttempts = 5;
// Fields
private bool m_enableOfflineCaching;
private double m_cacheRetryDelayInterval;
private int m_cacheMaximumRetryAttempts;
private WindowsPrincipal m_windowsPrincipal;
#endregion
#region [ Constructors ]
///
/// Initializes a new instance of the class.
///
/// Name that uniquely identifies the user.
public LdapSecurityProvider(string username)
: this(username, true, false, true)
{
}
///
/// Initializes a new instance of the class.
///
/// 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 LdapSecurityProvider(string username, bool canRefreshData, bool canResetPassword, bool canChangePassword)
: base(username, canRefreshData, canResetPassword, canChangePassword)
{
m_enableOfflineCaching = DefaultEnableOfflineCaching;
m_cacheRetryDelayInterval = DefaultCacheRetryDelayInterval;
m_cacheMaximumRetryAttempts = DefaultCacheMaximumRetryAttempts;
}
#endregion
#region [ Properties ]
///
/// Gets or sets a boolean value that indicates whether user information is to be cached for offline authentication.
///
public bool EnableOfflineCaching
{
get
{
return m_enableOfflineCaching;
}
set
{
m_enableOfflineCaching = value;
}
}
///
/// Gets or sets the wait interval (in milliseconds) before retrying load of offline user data cache.
///
public double CacheRetryDelayInterval
{
get
{
return m_cacheRetryDelayInterval;
}
set
{
m_cacheRetryDelayInterval = value;
}
}
///
/// Gets or sets the maximum retry attempts allowed for loading offline user data cache.
///
public int CacheMaximumRetryAttempts
{
get
{
return m_cacheMaximumRetryAttempts;
}
set
{
m_cacheMaximumRetryAttempts = value;
}
}
///
/// Gets the original of the user if the user exists in Active Directory.
///
public WindowsPrincipal WindowsPrincipal
{
get
{
return m_windowsPrincipal;
}
protected set
{
m_windowsPrincipal = value;
}
}
#endregion
#region [ Methods ]
#region [ Not Supported ]
///
/// Resets user password in the backend data store.
///
/// Answer to the user's security question.
/// true if the password is reset, otherwise false.
/// Always
public override bool ResetPassword(string securityAnswer)
{
throw new NotSupportedException();
}
#endregion
///
/// Saves settings to the config file if the property is set to true.
///
public override void SaveSettings()
{
base.SaveSettings();
if (PersistSettings)
{
// Save settings under the specified category.
ConfigurationFile config = ConfigurationFile.Current;
CategorizedSettingsElementCollection settings = config.Settings[SettingsCategory];
settings["EnableOfflineCaching", true].Update(m_enableOfflineCaching);
settings["CacheRetryDelayInterval", true].Update(m_cacheRetryDelayInterval);
settings["CacheMaximumRetryAttempts", true].Update(m_cacheMaximumRetryAttempts);
config.Save();
}
}
///
/// Loads saved settings from the config file if the property is set to true.
///
public override void LoadSettings()
{
base.LoadSettings();
if (PersistSettings)
{
// Load settings from the specified category.
ConfigurationFile config = ConfigurationFile.Current;
CategorizedSettingsElementCollection settings = config.Settings[SettingsCategory];
settings.Add("EnableOfflineCaching", m_enableOfflineCaching, "True to enable caching of user group information for authentication in offline state, otherwise False.");
settings.Add("CacheRetryDelayInterval", m_cacheRetryDelayInterval, "Wait interval, in milliseconds, before retrying load of user data cache.");
settings.Add("CacheMaximumRetryAttempts", m_cacheMaximumRetryAttempts, "Maximum retry attempts allowed for loading user data cache.");
EnableOfflineCaching = settings["EnableOfflineCaching"].ValueAs(m_enableOfflineCaching);
CacheRetryDelayInterval = settings["CacheRetryDelayInterval"].ValueAs(m_cacheRetryDelayInterval);
CacheMaximumRetryAttempts = settings["CacheMaximumRetryAttempts"].ValueAs(m_cacheMaximumRetryAttempts);
}
}
///
/// Authenticates the user.
///
/// true if the user is authenticated, otherwise false.
public override bool Authenticate()
{
// Check prerequisites
bool isValid =
(UserData.IsDefined && !UserData.IsDisabled && !UserData.IsLockedOut) &&
(UserData.PasswordChangeDateTime == DateTime.MinValue || UserData.PasswordChangeDateTime > DateTime.UtcNow);
if (!isValid)
return false;
if (string.IsNullOrEmpty(Password))
{
// Validate with passthrough credentials
m_windowsPrincipal = PassthroughPrincipal as WindowsPrincipal;
IsUserAuthenticated =
(object)m_windowsPrincipal != null &&
((!string.IsNullOrEmpty(UserData.LoginID) && m_windowsPrincipal.Identity.Name.Equals(UserData.LoginID, StringComparison.OrdinalIgnoreCase)) ||
(!string.IsNullOrEmpty(UserData.Username) && m_windowsPrincipal.Identity.Name.Equals(UserData.Username, StringComparison.OrdinalIgnoreCase))) &&
m_windowsPrincipal.Identity.IsAuthenticated;
}
else
{
// Validate by performing network logon
string[] userParts = UserData.LoginID.Split('\\');
m_windowsPrincipal = UserInfo.AuthenticateUser(userParts[0], userParts[1], Password) as WindowsPrincipal;
IsUserAuthenticated = (object)m_windowsPrincipal != null && m_windowsPrincipal.Identity.IsAuthenticated;
}
return IsUserAuthenticated;
}
///
/// Refreshes the from the backend data store.
///
/// true if is refreshed, otherwise false.
public override bool RefreshData()
{
// For consistency with WindowIdentity principal, user groups are loaded into Roles collection
UserData userData = new UserData(UserData.Username);
bool result = RefreshData(userData, userData.Roles, ProviderID);
if (result)
{
string[] parts;
// Remove domain name prefixes from user group names (again to match WindowIdentity principal implementation)
for (int i = 0; i < userData.Roles.Count; i++)
{
parts = userData.Roles[i].Split('\\');
if (parts.Length == 2)
userData.Roles[i] = parts[1];
}
}
UserData = userData;
return result;
}
///
/// Refreshes the from the backend data store loading user groups into desired collection.
///
/// The structure for the data being refreshed.
/// Target collection for user groups.
/// Unique provider ID used to distinguish cached user data that may be different based on provider.
/// true if is refreshed, otherwise false.
protected virtual bool RefreshData(UserData userData, List groupCollection, int providerID)
{
if ((object)groupCollection == null)
throw new ArgumentNullException(nameof(groupCollection));
if (string.IsNullOrEmpty(userData.Username))
return false;
// Initialize user data
userData.Initialize();
// Populate user data
UserInfo user = null;
UserDataCache userDataCache = null;
try
{
// Get current local user data cache
if (m_enableOfflineCaching)
{
userDataCache = UserDataCache.GetCurrentCache(providerID);
userDataCache.RetryDelayInterval = m_cacheRetryDelayInterval;
userDataCache.MaximumRetryAttempts = m_cacheMaximumRetryAttempts;
userDataCache.ReloadOnChange = false;
userDataCache.AutoSave = true;
userDataCache.Load();
}
// Create user info object using specified LDAP path if provided
string ldapPath = GetLdapPath();
if (string.IsNullOrEmpty(ldapPath))
user = new UserInfo(userData.Username);
else
user = new UserInfo(userData.Username, ldapPath);
// Make sure to load privileged user credentials from config file if present.
user.PersistSettings = true;
// If the system is currently logged in under a domain account and the system is disconnected from the domain,
// UserInfo will need access to the passthrough principal to determine whether the user exists on the domain
user.PassthroughPrincipal = PassthroughPrincipal;
// Attempt to determine if user exists (this will initialize user object if not initialized already)
userData.IsDefined = user.Exists;
userData.LoginID = user.LoginID;
// Fill in user information from domain data if it is available
if (!user.DomainRespondsForUser)
{
// Attempt to load previously cached user information when domain is offline
UserData cachedUserData;
if ((object)userDataCache != null && userDataCache.TryGetUserData(userData.LoginID, out cachedUserData))
{
// Copy relevant cached user information
userData.IsDefined = true;
userData.FirstName = cachedUserData.FirstName;
userData.LastName = cachedUserData.LastName;
userData.CompanyName = cachedUserData.CompanyName;
userData.PhoneNumber = cachedUserData.PhoneNumber;
userData.EmailAddress = cachedUserData.EmailAddress;
userData.IsLockedOut = cachedUserData.IsLockedOut;
userData.IsDisabled = cachedUserData.IsDisabled;
userData.Roles.AddRange(cachedUserData.Roles);
userData.Groups.AddRange(cachedUserData.Groups);
// If domain is offline, a password change cannot be initiated
userData.PasswordChangeDateTime = DateTime.MaxValue;
userData.AccountCreatedDateTime = cachedUserData.AccountCreatedDateTime;
}
else
{
// No previous user data was cached and domain is currently unavailable, however Windows could
// allow authentication using cached credentials when authenticating, so all we can do at this
// point is assume that user "could" exist and allow authentication to be attempted
userData.IsDefined = true;
userData.IsLockedOut = false;
userData.IsDisabled = false;
userData.PasswordChangeDateTime = DateTime.MaxValue;
userData.AccountCreatedDateTime = DateTime.MinValue;
}
}
else if (userData.IsDefined)
{
// Copy relevant user information
userData.FirstName = user.FirstName;
userData.LastName = user.LastName;
userData.CompanyName = user.Company;
userData.PhoneNumber = user.Telephone;
userData.EmailAddress = user.Email;
try
{
userData.IsLockedOut = user.AccountIsLockedOut;
userData.IsDisabled = user.AccountIsDisabled;
}
catch (SecurityException)
{
// AD may restrict information on account availability, if so, have to make a safe assumption:
userData.IsLockedOut = true;
userData.IsDisabled = true;
}
userData.PasswordChangeDateTime = user.NextPasswordChangeDate;
userData.AccountCreatedDateTime = user.AccountCreationDate;
// Assign all groups the user is a member of
foreach (string groupName in user.Groups)
{
if (!groupCollection.Contains(groupName, StringComparer.OrdinalIgnoreCase))
groupCollection.Add(groupName);
}
if ((object)userDataCache != null)
{
// Cache user data so that information can be loaded later if domain is unavailable
userDataCache[userData.LoginID] = userData;
// Wait for pending serialization since cache is scoped locally to this method and will be disposed before exit
userDataCache.WaitForSave();
}
}
return userData.IsDefined;
}
finally
{
if ((object)user != null)
{
user.PersistSettings = false;
user.Dispose();
}
if ((object)userDataCache != null)
userDataCache.Dispose();
}
}
///
/// Changes user password in the backend data store.
///
/// User's current password.
/// User's new password.
/// true if the password is changed, otherwise false.
///
/// This method always returns false under Mono deployments.
///
public override bool ChangePassword(string oldPassword, string newPassword)
{
// Check prerequisites
if (!UserData.IsDefined || UserData.IsDisabled || UserData.IsLockedOut)
return false;
UserInfo user = null;
WindowsImpersonationContext context = null;
try
{
string ldapPath = GetLdapPath();
// Create user info object using specified LDAP path if provided
if (string.IsNullOrEmpty(ldapPath))
user = new UserInfo(UserData.Username);
else
user = new UserInfo(UserData.Username, ldapPath);
// Initialize user entry
user.PersistSettings = true;
user.Initialize();
// Impersonate privileged user
context = user.ImpersonatePrivilegedAccount();
// Change user password
user.ChangePassword(oldPassword, newPassword);
return true;
}
catch (TargetInvocationException ex)
{
// Propagate password change error
if ((object)ex.InnerException == null)
throw new SecurityException(ex.Message, ex);
else
throw new SecurityException(ex.InnerException.Message, ex);
}
finally
{
if ((object)user != null)
user.Dispose();
if ((object)context != null)
UserInfo.EndImpersonation(context);
RefreshData();
}
}
///
/// Performs a translation of the specified user .
///
/// The user role to be translated.
/// The user role that the specified user translates to.
public override string TranslateRole(string role)
{
// Perform a translation from SID to Role only if the input starts with 'SID:'
if (role.StartsWith("SID:", StringComparison.OrdinalIgnoreCase))
return new SecurityIdentifier(role.Remove(0, 4)).Translate(typeof(NTAccount)).ToString().Split('\\')[1];
return role;
}
///
/// Gets the LDAP path.
///
/// The LDAP path.
protected virtual string GetLdapPath()
{
if (ConnectionString.StartsWith("LDAP://", StringComparison.OrdinalIgnoreCase) ||
ConnectionString.StartsWith("LDAPS://", StringComparison.OrdinalIgnoreCase))
return ConnectionString;
foreach (KeyValuePair pair in ConnectionString.ParseKeyValuePairs())
{
if (pair.Value.StartsWith("LDAP://", StringComparison.OrdinalIgnoreCase) ||
pair.Value.StartsWith("LDAPS://", StringComparison.OrdinalIgnoreCase))
return pair.Value;
}
return null;
}
///
/// 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 override List GetUserRoles(string applicationId)
{
return UserData?.Roles ?? new List();
}
#endregion
}
}