//****************************************************************************************************** // UserRoleCache.cs - Gbtc // // Copyright © 2013, 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 Eclipse Public License -v 1.0 (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/eclipse-1.0.php // // 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: // ---------------------------------------------------------------------------------------------------- // 05/07/2013 - J. Ritchie Carroll // Generated original version of source code. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.IO; using System.Security.Cryptography; using GSF.Collections; using GSF.IO; using GSF.Security.Cryptography; using GSF.Threading; namespace GSF.Security { /// /// Represents a secured interprocess cache for a of serialized user role information. /// public class UserRoleCache : InterprocessCache { #region [ Members ] // Constants // Default user role cache file name private const string DefaultCacheFileName = "UserRoleCache.bin"; // Fields private Dictionary m_userRoles; // Internal dictionary of serialized user roles private readonly object m_userRolesLock; // Lock object #endregion /// /// Creates a new instance of the with the specified number of . /// /// Maximum concurrent reader locks to allow. public UserRoleCache(int maximumConcurrentLocks = InterprocessReaderWriterLock.DefaultMaximumConcurrentLocks) : base(maximumConcurrentLocks) { m_userRoles = new Dictionary(); m_userRolesLock = new object(); } #region [ Properties ] /// /// Gets a copy of the internal user role dictionary. /// public Dictionary UserRoles { get { Dictionary userRoles; // We wait until the user roles cache is loaded before attempting to access it WaitForDataReady(); // Wait for thread level lock on dictionary lock (m_userRolesLock) { // Make a copy of the user role table for external use userRoles = new Dictionary(m_userRoles); } return userRoles; } } /// /// Gets or sets access role for given . /// /// User name for associated access role to load or save. /// Access role for given if found; otherwise null. public string this[string userName] { get { string role; TryGetUserRole(userName, out role); return role; } set { SaveUserRole(userName, value); } } #endregion #region [ Methods ] /// /// Attempts to retrieve access role for given . /// /// User name associated with access role to retrieve. /// Access role to populate if found. /// true if access role for given was retrieved; otherwise false. public bool TryGetUserRole(string userName, out string role) { string hash = HashLoginID(userName); bool result; // We wait until the cache is loaded before attempting to access it WaitForDataReady(); // Wait for thread level lock on user info table lock (m_userRolesLock) { // Attempt to lookup persisted access role based on hash of user name result = m_userRoles.TryGetValue(hash, out role); } return result; } /// /// Serializes the for the given into the . /// /// User name associated with access role to retrieve. /// Access role to update or populate. /// /// /// This will add an entry into the user info 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 SaveUserRole(string userName, string role) { string hash = HashLoginID(userName); // We wait until the cache is loaded before attempting to access it WaitForDataReady(); // Wait for thread level lock on user data table lock (m_userRolesLock) { // Assign new user information to user data table m_userRoles[hash] = role; } // Queue up a serialization for this new user information Save(); } /// /// Merge user roles from another , local cache taking precedence. /// /// Other to merge with. public void MergeLeft(UserRoleCache other) { // Merge other roles into local ones Dictionary mergedUserRoles = UserRoles.Merge(other.UserRoles); // Wait for thread level lock on dictionary lock (m_userRolesLock) { // Replace local user roles dictionary with merged roles m_userRoles = mergedUserRoles; } // Queue up a serialization for any newly added roles Save(); } /// /// Merge user roles from another , other cache taking precedence. /// /// Other to merge with. public void MergeRight(UserRoleCache other) { // Merge other roles into local ones Dictionary mergedUserRoles = other.UserRoles.Merge(UserRoles); // Wait for thread level lock on dictionary lock (m_userRolesLock) { // Replace local user roles dictionary with merged roles m_userRoles = mergedUserRoles; } // Queue up a serialization for any newly added roles Save(); } /// /// Initiates interprocess synchronized save of user role cache. /// public override void Save() { byte[] serializedUserDataTable; // Wait for thread level lock on dictionary lock (m_userRolesLock) { serializedUserDataTable = Serialization.Serialize(m_userRoles, SerializationFormat.Binary); } // File data is the serialized user roles dictionary, assigmnent 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 role 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[] serializedUserRoles = ProtectedData.Unprotect(fileStream.ReadStream(), null, DataProtectionScope.LocalMachine); Dictionary userRoles = Serialization.Deserialize>(serializedUserRoles, SerializationFormat.Binary); // Wait for thread level lock on user role dictionary lock (m_userRolesLock) { // Merge new and existing dictionaries since new user roles may have been queued for serialization, but not saved yet m_userRoles = userRoles.Merge(m_userRoles); } return serializedUserRoles; } /// /// Calculates the hash of the used as the key for the user roles dictionary. /// /// User name to hash. /// The Base64 encoded calculated SHA-2 hash of the used as the key for the user roles dictionary. /// /// For added security, a hash of the is used as the key for access roles /// in the user roles 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 userName) { return Cipher.GetPasswordHash(userName.ToLower(), 128); } // 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 role cache access failure: timeout while attempting to load user role cache.", ex); } } #endregion #region [ Static ] // Static Methods /// /// Loads the for the current local user. /// /// Loaded instance of the . public static UserRoleCache GetCurrentCache() { UserRoleCache currentCache; UserRoleCache localUserRoleCache; string localCacheFileName = FilePath.GetAbsolutePath(DefaultCacheFileName); // Initialize local user role cache (application may only have read-only access to this cache) localUserRoleCache = new UserRoleCache { FileName = localCacheFileName, #if DNF45 ReloadOnChange = true, #else // Reload on change is disabled to eliminate GC handle leaks on .NET 4.0, this prevents // automatic runtime reloading of key/iv data cached by another application. ReloadOnChange = false, #endif AutoSave = false }; // Load initial user roles localUserRoleCache.Load(); try { // Validate that user has write access to the local cryptographic cache folder string tempFile = FilePath.GetDirectoryName(localCacheFileName) + Guid.NewGuid().ToString() + ".tmp"; using (File.Create(tempFile)) { } if (File.Exists(tempFile)) File.Delete(tempFile); // No access issues exist, use local cache as the primary cryptographic key and initialization vector cache currentCache = localUserRoleCache; currentCache.AutoSave = true; } catch (UnauthorizedAccessException) { // User does not have needed serialization access to common cryptographic cache folder, // use a path where user will have rights string userCacheFolder = FilePath.AddPathSuffix(FilePath.GetApplicationDataFolder()); string userCacheFileName = userCacheFolder + FilePath.GetFileName(localCacheFileName); // Make sure user directory exists if (!Directory.Exists(userCacheFolder)) Directory.CreateDirectory(userCacheFolder); // Copy existing common cryptographic cache if none exists if (File.Exists(localCacheFileName) && !File.Exists(userCacheFileName)) File.Copy(localCacheFileName, userCacheFileName); // Initialize primary cryptographic key and initialization vector cache within user folder currentCache = new UserRoleCache { FileName = userCacheFileName, #if DNF45 ReloadOnChange = true, #else // Reload on change is disabled to eliminate GC handle leaks on .NET 4.0, this prevents // automatic runtime reloading of key/iv data cached by another application. ReloadOnChange = false, #endif AutoSave = true }; // Load initial keys currentCache.Load(); // Merge new or updated keys, protected folder keys taking precendence over user keys currentCache.MergeRight(localUserRoleCache); } return currentCache; } #endregion } }