Showing the changes to Source/Libraries/GSF.Security/UserDataCache.cs (1 of 628 files). Show all
Unified · Split
| @@ -0,0 +1,322 @@ | |||
| 1 | //****************************************************************************************************** | ||
| 2 | // UserDataCache.cs - Gbtc | ||
| 3 | // | ||
| 4 | // Copyright © 2010, Grid Protection Alliance. All Rights Reserved. | ||
| 5 | // | ||
| 6 | // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | ||
| 7 | // the NOTICE file distributed with this work for additional information regarding copyright ownership. | ||
| 8 | // The GPA licenses this file to you under the Eclipse Public License -v 1.0 (the "License"); you may | ||
| 9 | // not use this file except in compliance with the License. You may obtain a copy of the License at: | ||
| 10 | // | ||
| 11 | // http://www.opensource.org/licenses/eclipse-1.0.php | ||
| 12 | // | ||
| 13 | // Unless agreed to in writing, the subject software distributed under the License is distributed on an | ||
| 14 | // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | ||
| 15 | // License for the specific language governing permissions and limitations. | ||
| 16 | // | ||
| 17 | // Code Modification History: | ||
| 18 | // ---------------------------------------------------------------------------------------------------- | ||
| 19 | // 04/06/2011 - J. Ritchie Carroll | ||
| 20 | // Generated original version of source code. | ||
| 21 | // 04/14/2011 - Pinal C. Patel | ||
| 22 | // Updated to use new serialization and deserialization methods in GSF.Serialization class. | ||
| 23 | // 06/10/2011 - Pinal C. Patel | ||
| 24 | // Renamed RetryDelayInterval and MaximumRetryAttempts settings persisted to the config file | ||
| 25 | // to CacheRetryDelayInterval and CacheMaximumRetryAttempts for clarity. | ||
| 26 | // 08/12/2011 - J. Ritchie Carroll | ||
| 27 | // Modifed static GetCurrentCache to accept settings category of host security provider | ||
| 28 | // implementation in case the category has been changed from the default value by the consumer. | ||
| 29 | // 08/16/2011 - Pinal C. Patel | ||
| 30 | // Modified GetCurrentCache() to just set the FileName property and not the RetryDelayInterval, | ||
| 31 | // MaximumRetryAttempts, ReloadOnChange and AutoSave properties. | ||
| 32 | // 10/8/2012 - Danyelle Gilliam | ||
| 33 | // Modified Header | ||
| 34 | // | ||
| 35 | //****************************************************************************************************** | ||
| 36 | |||
| 37 | |||
| 38 | |||
| 39 | #region [ Contributor License Agreements ] | ||
| 40 | |||
| 41 | //****************************************************************************************************** | ||
| 42 | // | ||
| 43 | // Copyright © 2011, Grid Protection Alliance. All Rights Reserved. | ||
| 44 | // | ||
| 45 | // The GPA licenses this file to you under the Eclipse Public License -v 1.0 (the "License"); you may | ||
| 46 | // not use this file except in compliance with the License. You may obtain a copy of the License at: | ||
| 47 | // | ||
| 48 | // http://www.opensource.org/licenses/eclipse-1.0.php | ||
| 49 | // | ||
| 50 | // Unless agreed to in writing, the subject software distributed under the License is distributed on an | ||
| 51 | // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | ||
| 52 | // License for the specific language governing permissions and limitations. | ||
| 53 | // | ||
| 54 | //****************************************************************************************************** | ||
| 55 | |||
| 56 | #endregion | ||
| 57 | |||
| 58 | using GSF.Collections; | ||
| 59 | using GSF.IO; | ||
| 60 | using GSF.Security.Cryptography; | ||
| 61 | using GSF.Threading; | ||
| 62 | using System; | ||
| 63 | using System.Collections.Generic; | ||
| 64 | using System.IO; | ||
| 65 | using System.Security.Cryptography; | ||
| 66 | |||
| 67 | namespace GSF.Security | ||
| 68 | { | ||
| 69 | /// <summary> | ||
| 70 | /// Represents a secured interprocess cache for a <see cref="Dictionary{T1,T2}"/> of serialized <see cref="UserData"/>. | ||
| 71 | /// </summary> | ||
| 72 | public class UserDataCache : InterprocessCache | ||
| 73 | { | ||
| 74 | #region [ Members ] | ||
| 75 | |||
| 76 | // Constants | ||
| 77 | |||
| 78 | // Default key and initialization vector cache file name | ||
| 79 | private const string DefaultCacheFileName = "UserDataCache.bin"; | ||
| 80 | |||
| 81 | // Fields | ||
| 82 | private Dictionary<string, UserData> m_userDataTable; // Internal dictionary of serialized user data | ||
| 83 | private object m_userDataTableLock; // Lock object | ||
| 84 | private int m_providerID; // Unique provider ID used to distinguish cached user data that may be different based on provider | ||
| 85 | |||
| 86 | #endregion | ||
| 87 | |||
| 88 | /// <summary> | ||
| 89 | /// Creates a new instance of the <see cref="UserDataCache"/>. | ||
| 90 | /// </summary> | ||
| 91 | /// <param name="providerID">Unique provider ID used to distinguish cached user data that may be different based on provider.</param> | ||
| 92 | public UserDataCache(int providerID = LdapSecurityProvider.ProviderID) | ||
| 93 | : this(providerID, InterprocessReaderWriterLock.DefaultMaximumConcurrentLocks) | ||
| 94 | { | ||
| 95 | } | ||
| 96 | |||
| 97 | /// <summary> | ||
| 98 | /// Creates a new instance of the <see cref="UserDataCache"/> with the specified number of <paramref name="maximumConcurrentLocks"/>. | ||
| 99 | /// </summary> | ||
| 100 | /// <param name="maximumConcurrentLocks">Maximum concurrent reader locks to allow.</param> | ||
| 101 | /// <param name="providerID">Unique provider ID used to distinguish cached user data that may be different based on provider.</param> | ||
| 102 | public UserDataCache(int providerID, int maximumConcurrentLocks) | ||
| 103 | : base(maximumConcurrentLocks) | ||
| 104 | { | ||
| 105 | m_providerID = providerID; | ||
| 106 | m_userDataTable = new Dictionary<string, UserData>(); | ||
| 107 | m_userDataTableLock = new object(); | ||
| 108 | } | ||
| 109 | |||
| 110 | #region [ Properties ] | ||
| 111 | |||
| 112 | /// <summary> | ||
| 113 | /// Gets or sets <see cref="UserData"/> for given <paramref name="loginID"/>. | ||
| 114 | /// </summary> | ||
| 115 | /// <param name="loginID">Login ID of associated <see cref="UserData"/> to load or save.</param> | ||
| 116 | /// <returns>Reference to <see cref="UserData"/> for given <paramref name="loginID"/> if found; otherwise <c>null</c>.</returns> | ||
| 117 | public UserData this[string loginID] | ||
| 118 | { | ||
| 119 | get | ||
| 120 | { | ||
| 121 | UserData userData; | ||
| 122 | TryGetUserData(loginID, out userData); | ||
| 123 | return userData; | ||
| 124 | } | ||
| 125 | set | ||
| 126 | { | ||
| 127 | SaveUserData(loginID, value); | ||
| 128 | } | ||
| 129 | } | ||
| 130 | |||
| 131 | /// <summary> | ||
| 132 | /// Gets ot sets unique provider ID used to distinguish cached user data that may be different based on provider. | ||
| 133 | /// </summary> | ||
| 134 | public int ProviderID | ||
| 135 | { | ||
| 136 | get | ||
| 137 | { | ||
| 138 | return m_providerID; | ||
| 139 | } | ||
| 140 | set | ||
| 141 | { | ||
| 142 | m_providerID = value; | ||
| 143 | } | ||
| 144 | } | ||
| 145 | |||
| 146 | #endregion | ||
| 147 | |||
| 148 | #region [ Methods ] | ||
| 149 | |||
| 150 | /// <summary> | ||
| 151 | /// Attempts to retrieve <see cref="UserData"/> for given <paramref name="loginID"/>. | ||
| 152 | /// </summary> | ||
| 153 | /// <param name="loginID">Login ID of associated <see cref="UserData"/> to retrieve.</param> | ||
| 154 | /// <param name="userData">Reference to <see cref="UserData"/> object to populate if found.</param> | ||
| 155 | /// <returns><c>true</c> if <see cref="UserData"/> for given <paramref name="loginID"/> was retrieved; otherwise <c>false</c>.</returns> | ||
| 156 | public bool TryGetUserData(string loginID, out UserData userData) | ||
| 157 | { | ||
| 158 | string hash = HashLoginID(loginID); | ||
| 159 | bool result; | ||
| 160 | |||
| 161 | // We wait until the cache is loaded before attempting to access it | ||
| 162 | WaitForDataReady(); | ||
| 163 | |||
| 164 | // Wait for thread level lock on user data table | ||
| 165 | lock (m_userDataTableLock) | ||
| 166 | { | ||
| 167 | // Attempt to lookup persisted user data based on hash of login ID | ||
| 168 | result = m_userDataTable.TryGetValue(hash, out userData); | ||
| 169 | } | ||
| 170 | |||
| 171 | return result; | ||
| 172 | } | ||
| 173 | |||
| 174 | /// <summary> | ||
| 175 | /// Serializes the <paramref name="userData"/> for the given <paramref name="loginID"/> into the <see cref="UserDataCache"/>. | ||
| 176 | /// </summary> | ||
| 177 | /// <param name="loginID">Login ID of associated <see cref="UserData"/> to retrieve.</param> | ||
| 178 | /// <param name="userData">Reference to <see cref="UserData"/> object to serialize into <see cref="UserDataCache"/>.</param> | ||
| 179 | /// <remarks> | ||
| 180 | /// <para> | ||
| 181 | /// This will add an entry into the user data cache for <paramref name="loginID"/> if it doesn't exist; | ||
| 182 | /// otherwise existing entry will be updated. | ||
| 183 | /// </para> | ||
| 184 | /// <para> | ||
| 185 | /// Updates are automatically queued up for serialization so user does not need to call <see cref="Save"/>. | ||
| 186 | /// </para> | ||
| 187 | /// </remarks> | ||
| 188 | public void SaveUserData(string loginID, UserData userData) | ||
| 189 | { | ||
| 190 | string hash = HashLoginID(loginID); | ||
| 191 | |||
| 192 | // We wait until the cache is loaded before attempting to access it | ||
| 193 | WaitForDataReady(); | ||
| 194 | |||
| 195 | // Wait for thread level lock on user data table | ||
| 196 | lock (m_userDataTableLock) | ||
| 197 | { | ||
| 198 | // Assign new user information to user data table | ||
| 199 | m_userDataTable[hash] = userData; | ||
| 200 | } | ||
| 201 | |||
| 202 | // Queue up a serialization for this new user information | ||
| 203 | Save(); | ||
| 204 | } | ||
| 205 | |||
| 206 | /// <summary> | ||
| 207 | /// Initiates interprocess synchronized save of user data cache. | ||
| 208 | /// </summary> | ||
| 209 | public override void Save() | ||
| 210 | { | ||
| 211 | byte[] serializedUserDataTable; | ||
| 212 | |||
| 213 | // Wait for thread level lock on key table | ||
| 214 | lock (m_userDataTableLock) | ||
| 215 | { | ||
| 216 | serializedUserDataTable = Serialization.Serialize(m_userDataTable, SerializationFormat.Binary); | ||
| 217 | } | ||
| 218 | |||
| 219 | // File data is the serialized user data table, assigmnent will initiate auto-save if needed | ||
| 220 | FileData = serializedUserDataTable; | ||
| 221 | } | ||
| 222 | |||
| 223 | /// <summary> | ||
| 224 | /// Handles serialization of file to disk; virtual method allows customization (e.g., pre-save encryption and/or data merge). | ||
| 225 | /// </summary> | ||
| 226 | /// <param name="fileStream"><see cref="FileStream"/> used to serialize data.</param> | ||
| 227 | /// <param name="fileData">File data to be serialized.</param> | ||
| 228 | /// <remarks> | ||
| 229 | /// Consumers overriding this method should not directly call <see cref="InterprocessCache.FileData"/> property to avoid potential dead-locks. | ||
| 230 | /// </remarks> | ||
| 231 | protected override void SaveFileData(FileStream fileStream, byte[] fileData) | ||
| 232 | { | ||
| 233 | // Encrypt data local to this machine (this way user cannot copy user data cache to another machine) | ||
| 234 | base.SaveFileData(fileStream, ProtectedData.Protect(fileData, null, DataProtectionScope.LocalMachine)); | ||
| 235 | } | ||
| 236 | |||
| 237 | /// <summary> | ||
| 238 | /// Handles deserialization of file from disk; virtual method allows customization (e.g., pre-load decryption and/or data merge). | ||
| 239 | /// </summary> | ||
| 240 | /// <param name="fileStream"><see cref="FileStream"/> used to deserialize data.</param> | ||
| 241 | /// <returns>Deserialized file data.</returns> | ||
| 242 | /// <remarks> | ||
| 243 | /// Consumers overriding this method should not directly call <see cref="InterprocessCache.FileData"/> property to avoid potential dead-locks. | ||
| 244 | /// </remarks> | ||
| 245 | protected override byte[] LoadFileData(FileStream fileStream) | ||
| 246 | { | ||
| 247 | // Decrypt data that was encrypted local to this machine | ||
| 248 | byte[] serializedUserDataTable = ProtectedData.Unprotect(fileStream.ReadStream(), null, DataProtectionScope.LocalMachine); | ||
| 249 | Dictionary<string, UserData> userDataTable = Serialization.Deserialize<Dictionary<string, UserData>>(serializedUserDataTable, SerializationFormat.Binary); | ||
| 250 | |||
| 251 | // Wait for thread level lock on user data table | ||
| 252 | lock (m_userDataTableLock) | ||
| 253 | { | ||
| 254 | // Merge new and existing key tables since new user information may have been queued for serialization, but not saved yet | ||
| 255 | m_userDataTable = userDataTable.Merge(m_userDataTable); | ||
| 256 | } | ||
| 257 | |||
| 258 | return serializedUserDataTable; | ||
| 259 | } | ||
| 260 | |||
| 261 | /// <summary> | ||
| 262 | /// Calculates the hash of the <paramref name="loginID"/> used as the key for the user data cache. | ||
| 263 | /// </summary> | ||
| 264 | /// <param name="loginID">Login ID to hash.</param> | ||
| 265 | /// <returns>The Base64 encoded calculated SHA-2 hash of the <paramref name="loginID"/> used as the key for the user data cache.</returns> | ||
| 266 | /// <remarks> | ||
| 267 | /// For added security, a hash of the <paramref name="loginID"/> is used as the key for <see cref="UserData"/> in the | ||
| 268 | /// user data cache instead of the actual <paramref name="loginID"/>. This method allows the | ||
| 269 | /// consumer to properly calculate this hash when directly using the user data cache. | ||
| 270 | /// </remarks> | ||
| 271 | protected string HashLoginID(string loginID) | ||
| 272 | { | ||
| 273 | return Cipher.GetPasswordHash(loginID.ToLower(), m_providerID); | ||
| 274 | } | ||
| 275 | |||
| 276 | // Waits until the cache is loaded before attempting to access it | ||
| 277 | private void WaitForDataReady() | ||
| 278 | { | ||
| 279 | try | ||
| 280 | { | ||
| 281 | // Just wrapping this method to provide a more detailed exception message if there is an issue loading cache | ||
| 282 | WaitForLoad(); | ||
| 283 | } | ||
| 284 | catch (Exception ex) | ||
| 285 | { | ||
| 286 | throw new UnauthorizedAccessException("User data access failure: timeout while attempting to load user data cache.", ex); | ||
| 287 | } | ||
| 288 | } | ||
| 289 | |||
| 290 | #endregion | ||
| 291 | |||
| 292 | #region [ Static ] | ||
| 293 | |||
| 294 | // Static Methods | ||
| 295 | |||
| 296 | /// <summary> | ||
| 297 | /// Loads the <see cref="UserDataCache"/> for the current local user. | ||
| 298 | /// </summary> | ||
| 299 | /// <param name="providerID">Unique security provider ID used to distinguish cached user data that may be different based on provider.</param> | ||
| 300 | /// <returns>Loaded instance of the <see cref="UserDataCache"/>.</returns> | ||
| 301 | public static UserDataCache GetCurrentCache(int providerID) | ||
| 302 | { | ||
| 303 | // By default user data cache is stored in a path where user will have rights | ||
| 304 | UserDataCache userDataCache; | ||
| 305 | string userCacheFolder = FilePath.GetApplicationDataFolder(); | ||
| 306 | string userCacheFileName = Path.Combine(userCacheFolder, FilePath.GetFileName(DefaultCacheFileName)); | ||
| 307 | |||
| 308 | // Make sure user directory exists | ||
| 309 | if (!Directory.Exists(userCacheFolder)) | ||
| 310 | Directory.CreateDirectory(userCacheFolder); | ||
| 311 | |||
| 312 | // Initialize user data cache for current local user | ||
| 313 | userDataCache = new UserDataCache(providerID); | ||
| 314 | userDataCache.FileName = userCacheFileName; | ||
| 315 | |||
| 316 | return userDataCache; | ||
| 317 | } | ||
| 318 | |||
| 319 | #endregion | ||
| 320 | |||
| 321 | } | ||
| 322 | } | ||