//******************************************************************************************************
// 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 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:
// ----------------------------------------------------------------------------------------------------
// 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
// Modifed 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 GSF.Collections;
using GSF.IO;
using GSF.Security.Cryptography;
using GSF.Threading;
namespace GSF.Security
{
///
/// Represents a secured interprocess cache for a of serialized .
///
public class UserDataCache : InterprocessCache
{
#region [ Members ]
// Constants
// Default user AD group data cache file name
private const string DefaultCacheFileName = "UserDataCache.bin";
// Fields
private Dictionary m_userDataTable; // Internal dictionary of serialized user data
private readonly object m_userDataTableLock; // Lock object
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();
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 ot 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 interprocess synchronized save of user data cache.
///
public override void Save()
{
byte[] serializedUserDataTable;
// Wait for thread level lock on key table
lock (m_userDataTableLock)
{
serializedUserDataTable = Serialization.Serialize(m_userDataTable, SerializationFormat.Binary);
}
// File data is the serialized user data table, 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 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 = Serialization.Deserialize>(serializedUserDataTable, SerializationFormat.Binary);
// 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;
}
#endregion
}
}