//****************************************************************************************************** // 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 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: // ---------------------------------------------------------------------------------------------------- // 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 inter-process cache for a of serialized user role information. /// /// /// This is a system cache that contains the role assignments for each user that has logged in successfully. This cache is used /// to check for changes in role assignments for a user - that is, a role change in the database that may now be different than /// what is in the current cache. Any kind of role changes are logged as security events in the Windows event log for auditing. /// Note that this is kept as a separate cache from the since the user role cache is used for /// auditing and contains information relative to a user's roles at last login. /// 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 for internal dictionary #endregion #region [ Constructors ] /// /// 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(); } #endregion #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 dictionary for external use userRoles = new Dictionary(m_userRoles); } return userRoles; } } /// /// Gets or sets access roles for given . /// /// User name for associated access role to load or save. /// Access roles for given if found; otherwise null. public string[] this[string userName] { get { string[] roles; TryGetUserRole(userName, out roles); return roles; } set { SaveUserRole(userName, value); } } #endregion #region [ Methods ] /// /// Attempts to retrieve access role for given . /// /// User name associated with access role to retrieve. /// Access roles to populate if found. /// true if access roles for given were retrieved; otherwise false. public bool TryGetUserRole(string userName, out string[] roles) { 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 dictionary lock (m_userRolesLock) { // Attempt to lookup persisted access role based on hash of user name result = m_userRoles.TryGetValue(hash, out roles); } return result; } /// /// Serializes the for the given into the . /// /// User name associated with access role to retrieve. /// Access roles to update or populate. /// /// /// This will add an entry into the user roles 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[] roles) { 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 dictionary lock (m_userRolesLock) { // Assign new user information to user data dictionary m_userRoles[hash] = roles; } // 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 inter-process 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, 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 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 accessing 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, ReloadOnChange = false, AutoSave = false }; // Load initial user roles localUserRoleCache.Load(); try { // Validate that user has write access to the local cache folder string tempFile = FilePath.GetDirectoryName(localCacheFileName) + Guid.NewGuid() + ".tmp"; using (File.Create(tempFile)) { } if (File.Exists(tempFile)) File.Delete(tempFile); // No access issues exist, use local cache as the primary cache currentCache = localUserRoleCache; currentCache.AutoSave = true; localUserRoleCache = null; } catch (UnauthorizedAccessException) { // User does not have needed serialization access to common 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 cache if none exists if (File.Exists(localCacheFileName) && !File.Exists(userCacheFileName)) File.Copy(localCacheFileName, userCacheFileName); // Initialize primary cache within user folder currentCache = new UserRoleCache { FileName = userCacheFileName, ReloadOnChange = false, AutoSave = true }; // Load initial roles currentCache.Load(); // Merge new or updated roles, protected folder roles taking precedence over user folder roles currentCache.MergeRight(localUserRoleCache); } if ((object)localUserRoleCache != null) localUserRoleCache.Dispose(); return currentCache; } #endregion } }