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