//****************************************************************************************************** // UserDataCache.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: // ---------------------------------------------------------------------------------------------------- // 04/06/2011 - J. Ritchie Carroll // Generated original version of source code. // 04/14/2011 - Pinal C. Patel // Updated to use new serialization and deserialization methods in GSF.Serialization class. // 06/10/2011 - Pinal C. Patel // Renamed RetryDelayInterval and MaximumRetryAttempts settings persisted to the config file // to CacheRetryDelayInterval and CacheMaximumRetryAttempts for clarity. // 08/12/2011 - J. Ritchie Carroll // Modified static GetCurrentCache to accept settings category of host security provider // implementation in case the category has been changed from the default value by the consumer. // 08/16/2011 - Pinal C. Patel // Modified GetCurrentCache() to just set the FileName property and not the RetryDelayInterval, // MaximumRetryAttempts, ReloadOnChange and AutoSave properties. // 12/20/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.IO; using System.Security.Cryptography; using System.Text; using GSF.Collections; using GSF.IO; using GSF.Security.Cryptography; using GSF.Threading; namespace GSF.Security { /// /// Represents a secured inter-process cache for a of serialized . /// /// /// This is a personal user data cache that only contains basic LDAP information for the user. It is used to load the local and /// Active Directory groups a user is associated with when the user no longer has access to its domain server. This can happen /// when a laptop that is normally connected to the Active Directory domain gets shutdown then restarted without access to the /// domain, for example, on an airplane - in this mode the user can successfully still login to the laptop to their using domain /// account cached by Windows but the groups the user is in will no longer be accessible. If role based security happens to be /// based on Active Directory groups, this cache will make sure the user can still have needed role based access even when the /// domain is unavailable. This cache is maintained as a separate user cache from the system level /// since the user data cache only contains group information and is used by the which can be /// used independently of the . /// public class UserDataCache : InterprocessCache { #region [ Members ] // Constants // Default user AD group data cache file name private const string DefaultCacheFileName = "UserDataCache.bin"; // Expected cache header bytes private static readonly byte[] CacheHeaderBytes = { (byte)0x55, (byte)0x44, (byte)0x43 }; // Fields private Dictionary m_userDataTable; // Internal dictionary of serialized user data private readonly object m_userDataTableLock; // Lock object for internal dictionary private int m_providerID; // Unique provider ID used to distinguish cached user data that may be different based on provider #endregion /// /// Creates a new instance of the . /// /// Unique provider ID used to distinguish cached user data that may be different based on provider. public UserDataCache(int providerID = LdapSecurityProvider.ProviderID) : this(providerID, InterprocessReaderWriterLock.DefaultMaximumConcurrentLocks) { } /// /// Creates a new instance of the with the specified number of . /// /// Maximum concurrent reader locks to allow. /// Unique provider ID used to distinguish cached user data that may be different based on provider. public UserDataCache(int providerID, int maximumConcurrentLocks) : base(maximumConcurrentLocks) { m_providerID = providerID; m_userDataTable = new Dictionary(StringComparer.OrdinalIgnoreCase); m_userDataTableLock = new object(); } #region [ Properties ] /// /// Gets or sets for given . /// /// Login ID of associated to load or save. /// Reference to for given if found; otherwise null. public UserData this[string loginID] { get { UserData userData; TryGetUserData(loginID, out userData); return userData; } set { SaveUserData(loginID, value); } } /// /// Gets or sets unique provider ID used to distinguish cached user data that may be different based on provider. /// public int ProviderID { get { return m_providerID; } set { m_providerID = value; } } #endregion #region [ Methods ] /// /// Attempts to retrieve for given . /// /// Login ID of associated to retrieve. /// Reference to object to populate if found. /// true if for given was retrieved; otherwise false. public bool TryGetUserData(string loginID, out UserData userData) { string hash = HashLoginID(loginID); bool result; // We wait until the cache is loaded before attempting to access it WaitForDataReady(); // Wait for thread level lock on user data table lock (m_userDataTableLock) { // Attempt to lookup persisted user data based on hash of login ID result = m_userDataTable.TryGetValue(hash, out userData); } return result; } /// /// Serializes the for the given into the . /// /// Login ID of associated to retrieve. /// Reference to object to serialize into . /// /// /// This will add an entry into the user data cache for if it doesn't exist; /// otherwise existing entry will be updated. /// /// /// Updates are automatically queued up for serialization so user does not need to call . /// /// public void SaveUserData(string loginID, UserData userData) { string hash = HashLoginID(loginID); // We wait until the cache is loaded before attempting to access it WaitForDataReady(); // Wait for thread level lock on user data table lock (m_userDataTableLock) { // Assign new user information to user data table m_userDataTable[hash] = userData; } // Queue up a serialization for this new user information Save(); } /// /// Initiates inter-process synchronized save of user data cache. /// public override void Save() { byte[] serializedUserDataTable; // Wait for thread level lock on key table lock (m_userDataTableLock) { serializedUserDataTable = SerializeCache(m_userDataTable); } // File data is the serialized user data table, assignment will initiate auto-save if needed FileData = serializedUserDataTable; } /// /// Handles serialization of file to disk; virtual method allows customization (e.g., pre-save encryption and/or data merge). /// /// used to serialize data. /// File data to be serialized. /// /// Consumers overriding this method should not directly call property to avoid potential dead-locks. /// protected override void SaveFileData(FileStream fileStream, byte[] fileData) { // Encrypt data local to this machine (this way user cannot copy user data cache to another machine) base.SaveFileData(fileStream, ProtectedData.Protect(fileData, null, DataProtectionScope.LocalMachine)); } /// /// Handles deserialization of file from disk; virtual method allows customization (e.g., pre-load decryption and/or data merge). /// /// used to deserialize data. /// Deserialized file data. /// /// Consumers overriding this method should not directly call property to avoid potential dead-locks. /// protected override byte[] LoadFileData(FileStream fileStream) { // Decrypt data that was encrypted local to this machine byte[] serializedUserDataTable = ProtectedData.Unprotect(fileStream.ReadStream(), null, DataProtectionScope.LocalMachine); Dictionary userDataTable = DeserializeCache(serializedUserDataTable); // Wait for thread level lock on user data table lock (m_userDataTableLock) { // Merge new and existing key tables since new user information may have been queued for serialization, but not saved yet m_userDataTable = userDataTable.Merge(m_userDataTable); } return serializedUserDataTable; } /// /// Calculates the hash of the used as the key for the user data cache. /// /// Login ID to hash. /// The Base64 encoded calculated SHA-2 hash of the used as the key for the user data cache. /// /// For added security, a hash of the is used as the key for in the /// user data cache instead of the actual . This method allows the /// consumer to properly calculate this hash when directly using the user data cache. /// protected string HashLoginID(string loginID) { return Cipher.GetPasswordHash(loginID.ToLower(), m_providerID); } // Waits until the cache is loaded before attempting to access it private void WaitForDataReady() { try { // Just wrapping this method to provide a more detailed exception message if there is an issue loading cache WaitForLoad(); } catch (Exception ex) { throw new UnauthorizedAccessException("User data access failure: timeout while attempting to load user data cache.", ex); } } #endregion #region [ Static ] // Static Methods /// /// Loads the for the current local user. /// /// Unique security provider ID used to distinguish cached user data that may be different based on provider. /// Loaded instance of the . public static UserDataCache GetCurrentCache(int providerID) { // By default user data cache is stored in a path where user will have rights UserDataCache userDataCache; string userCacheFolder = FilePath.GetApplicationDataFolder(); string userCacheFileName = Path.Combine(userCacheFolder, FilePath.GetFileName(DefaultCacheFileName)); // Make sure user directory exists if (!Directory.Exists(userCacheFolder)) Directory.CreateDirectory(userCacheFolder); // Initialize user data cache for current local user userDataCache = new UserDataCache(providerID); userDataCache.FileName = userCacheFileName; return userDataCache; } private static byte[] SerializeCache(Dictionary cache) { using (BlockAllocatedMemoryStream stream = new BlockAllocatedMemoryStream()) using (BinaryWriter writer = new BinaryWriter(stream, Encoding.Default)) { writer.Write(CacheHeaderBytes); writer.Write(cache.Count); foreach (KeyValuePair data in cache) { UserData userData = data.Value; writer.Write(data.Key); writer.Write(userData.Username); writer.Write(userData.FirstName); writer.Write(userData.LastName); writer.Write(userData.CompanyName); writer.Write(userData.PhoneNumber); writer.Write(userData.EmailAddress); writer.Write(userData.IsLockedOut); writer.Write(userData.IsDisabled); writer.Write(userData.PasswordChangeDateTime.Ticks); writer.Write(userData.AccountCreatedDateTime.Ticks); writer.Write(userData.Roles.Count); foreach (string role in userData.Roles) writer.Write(role); writer.Write(userData.Groups.Count); foreach (string group in userData.Groups) writer.Write(group); } return stream.ToArray(); } } private Dictionary DeserializeCache(byte[] data) { Dictionary cache = new Dictionary(StringComparer.OrdinalIgnoreCase); using (MemoryStream stream = new MemoryStream(data)) using (BinaryReader reader = new BinaryReader(stream, Encoding.Default)) { if (reader.ReadBytes(CacheHeaderBytes.Length).CompareTo(CacheHeaderBytes) != 0) throw new InvalidDataException("Unexpected data read from UserDataCache - file possibly corrupted."); int listLength, cacheLength = reader.ReadInt32(); for (int i = 0; i < cacheLength; i++) { UserData userData = new UserData(); string loginID; loginID = reader.ReadString(); userData.Username = reader.ReadString(); userData.FirstName = reader.ReadString(); userData.LastName = reader.ReadString(); userData.CompanyName = reader.ReadString(); userData.PhoneNumber = reader.ReadString(); userData.EmailAddress = reader.ReadString(); userData.IsLockedOut = reader.ReadBoolean(); userData.IsDisabled = reader.ReadBoolean(); userData.PasswordChangeDateTime = new DateTime(reader.ReadInt64()); userData.AccountCreatedDateTime = new DateTime(reader.ReadInt64()); userData.Roles = new List(); listLength = reader.ReadInt32(); for (int j = 0; j < listLength; j++) userData.Roles.Add(reader.ReadString()); userData.Groups = new List(); listLength = reader.ReadInt32(); for (int j = 0; j < listLength; j++) userData.Groups.Add(reader.ReadString()); cache.Add(loginID, userData); } } return cache; } #endregion } }