History

Updated AdoSecurityProvider to cache last known security context such that security system can initialize without database access.

ritchiecarroll committed 12 years ago

Commit d0046311 parent 1a8fe7514 files · +1042 −427Browse files

Unified · Split

14 files
  1. Source/Libraries/GSF.Core/Data/DataSetEqualityComparer.cs +2 −0
  2. Source/Libraries/GSF.Core/IO/InterprocessCache.cs +141 −140
  3. Source/Libraries/GSF.Core/Security/Cryptography/Cipher.cs +3 −7
  4. Source/Libraries/GSF.Core/Threading/InterprocessLock.cs +2 −2
  5. Source/Libraries/GSF.Core/Threading/InterprocessReaderWriterLock.cs +24 −16
  6. Source/Libraries/GSF.Security/AdoSecurityCache.cs +296 −0
  7. Source/Libraries/GSF.Security/AdoSecurityProvider.cs +362 −157
  8. Source/Libraries/GSF.Security/GSF.Security.csproj +1 −0
  9. Source/Libraries/GSF.Security/LdapSecurityProvider.cs +29 −28
  10. Source/Libraries/GSF.Security/SecurityProviderBase.cs +3 −3
  11. Source/Libraries/GSF.Security/UserData.cs +99 −23
  12. Source/Libraries/GSF.Security/UserDataCache.cs +17 −7
  13. Source/Libraries/GSF.Security/UserRoleCache.cs +51 −44
  14. Source/Libraries/GSF.TimeSeries/ServiceHostBase.cs +12 −0
Source/Libraries/GSF.Core/Data/DataSetEqualityComparer.cs+2−0
@@ -124,6 +124,8 @@ namespace GSF.Data · 123 unmodified lines
124124 if ((object)obj == null)
125125 throw new ArgumentNullException("obj");
126126
127+ // This method is required by IEqualityComparer<T> - although the following
128+ // should work, this is not likely the most efficient way to do this...
127129 foreach (DataTable table in obj.Tables)
128130 {
129131 foreach (DataRow row in table.Rows)
Source/Libraries/GSF.Core/IO/InterprocessCache.cs+141−140
@@ -37,11 +37,11 @@ using Timer = System.Timers.Timer; · 36 unmodified lines
3737 namespace GSF.IO
3838 {
3939 /// <summary>
40− /// Represents a serialized data cache that can be saved or read from multiple applications using interprocess synchronization.
40+ /// Represents a serialized data cache that can be saved or read from multiple applications using inter-process synchronization.
4141 /// </summary>
4242 /// <remarks>
4343 /// Note that all file data in this class gets serialized to and from memory, as such, the design intention for this class is for
44− /// use with smaller data sets such as serialized lists or dictionaries that need interprocess synchronized loading and saving.
44+ /// use with smaller data sets such as serialized lists or dictionaries that need inter-process synchronized loading and saving.
4545 /// </remarks>
4646 public class InterprocessCache : IDisposable
4747 {
@@ -62,18 +62,18 @@ namespace GSF.IO · 14 unmodified lines
6262 public const double DefaultRetryDelayInterval = 200.0D;
6363
6464 // Fields
65− private string m_fileName; // Path and file name of file needing interprocess synchronization
65+ private string m_fileName; // Path and file name of file needing inter-process synchronization
6666 private byte[] m_fileData; // Data loaded or to be saved
6767 private bool m_autoSave; // Flag to auto save when file data has changed
68− private InterprocessReaderWriterLock m_fileLock; // Interprocess reader/writer lock used to synchronize file access
68+ private InterprocessReaderWriterLock m_fileLock; // Inter-process reader/writer lock used to synchronize file access
6969 private ReaderWriterLockSlim m_dataLock; // Thread level reader/writer lock used to synchronize file data access
7070 private ManualResetEventSlim m_loadIsReady; // Wait handle used so that system will wait for file data load
7171 private ManualResetEventSlim m_saveIsReady; // Wait handle used so that system will wait for file data save
7272 private FileSystemWatcher m_fileWatcher; // Optional file watcher used to reload changes
73− private readonly int m_maximumConcurrentLocks; // Maximum concurrent reader locks allowed
73+ private readonly int m_maximumConcurrentLocks; // Maximum concurrent reader locks allowed
7474 private int m_maximumRetryAttempts; // Maximum retry attempts allowed for loading file
75− private readonly BitArray m_retryQueue; // Retry event queue
76− private Timer m_retryTimer; // File I/O retry timer
75+ private readonly BitArray m_retryQueue; // Retry event queue
76+ private Timer m_retryTimer; // File I/O retry timer
7777 private long m_lastRetryTime; // Time of last retry attempt
7878 private int m_retryCount; // Total number of retries attempted so far
7979 private bool m_disposed; // Class disposed flag
@@ -125,9 +125,9 @@ namespace GSF.IO · 45 unmodified lines
125125 #region [ Properties ]
126126
127127 /// <summary>
128− /// Path and file name for the cache needing interprocess synchronization.
128+ /// Path and file name for the cache needing inter-process synchronization.
129129 /// </summary>
130− public virtual string FileName
130+ public string FileName
131131 {
132132 get
133133 {
@@ -136,7 +136,7 @@ namespace GSF.IO · 2 unmodified lines
136136 set
137137 {
138138 if ((object)value == null)
139− throw new ArgumentNullException("FileName", "FileName cannot be null");
139+ throw new NullReferenceException("FileName cannot be null");
140140
141141 m_fileName = FilePath.GetAbsolutePath(value);
142142
@@ -154,7 +154,7 @@ namespace GSF.IO · 11 unmodified lines
154154 /// <remarks>
155155 /// Setting value to <c>null</c> will create a zero-length file.
156156 /// </remarks>
157− public virtual byte[] FileData
157+ public byte[] FileData
158158 {
159159 get
160160 {
@@ -179,7 +179,7 @@ namespace GSF.IO · 18 unmodified lines
179179 set
180180 {
181181 if ((object)m_fileName == null)
182− throw new ArgumentNullException("FileName", "FileName property must be defined before setting FileData");
182+ throw new NullReferenceException("FileName property must be defined before setting FileData");
183183
184184 bool dataChanged = false;
185185
@@ -213,7 +213,7 @@ namespace GSF.IO · 27 unmodified lines
213213 /// <summary>
214214 /// Gets or sets flag that determines if <see cref="InterprocessCache"/> should automatically initiate a save when <see cref="FileData"/> has been updated.
215215 /// </summary>
216− public virtual bool AutoSave
216+ public bool AutoSave
217217 {
218218 get
219219 {
@@ -228,7 +228,7 @@ namespace GSF.IO · 8 unmodified lines
228228 /// <summary>
229229 /// Gets or sets flag that enables system to monitor for changes in <see cref="FileName"/> and automatically reload <see cref="FileData"/>.
230230 /// </summary>
231− public virtual bool ReloadOnChange
231+ public bool ReloadOnChange
232232 {
233233 get
234234 {
@@ -239,7 +239,7 @@ namespace GSF.IO · 4 unmodified lines
239239 if (value && (object)m_fileWatcher == null)
240240 {
241241 if ((object)m_fileName == null)
242− throw new ArgumentNullException("FileName", "FileName property must be defined before enabling ReloadOnChange");
242+ throw new NullReferenceException("FileName property must be defined before enabling ReloadOnChange");
243243
244244 // Setup file watcher to monitor for external updates
245245 m_fileWatcher = new FileSystemWatcher();
@@ -261,7 +261,7 @@ namespace GSF.IO · 15 unmodified lines
261261 /// <summary>
262262 /// Gets the maximum concurrent reader locks allowed.
263263 /// </summary>
264− public virtual int MaximumConcurrentLocks
264+ public int MaximumConcurrentLocks
265265 {
266266 get
267267 {
@@ -272,7 +272,7 @@ namespace GSF.IO · 4 unmodified lines
272272 /// <summary>
273273 /// Maximum retry attempts allowed for loading or saving cache file data.
274274 /// </summary>
275− public virtual int MaximumRetryAttempts
275+ public int MaximumRetryAttempts
276276 {
277277 get
278278 {
@@ -287,7 +287,7 @@ namespace GSF.IO · 8 unmodified lines
287287 /// <summary>
288288 /// Wait interval, in milliseconds, before retrying load or save of cache file data.
289289 /// </summary>
290− public virtual double RetryDelayInterval
290+ public double RetryDelayInterval
291291 {
292292 get
293293 {
@@ -368,7 +368,7 @@ namespace GSF.IO · 74 unmodified lines
368368 }
369369
370370 /// <summary>
371− /// Initiates interprocess synchronized cache file save.
371+ /// Initiates inter-process synchronized cache file save.
372372 /// </summary>
373373 /// <remarks>
374374 /// Subclasses should always call <see cref="WaitForLoad()"/> before calling this method.
@@ -376,17 +376,17 @@ namespace GSF.IO · 1 unmodified line
376376 public virtual void Save()
377377 {
378378 if ((object)m_fileName == null)
379− throw new ArgumentNullException("FileName", "FileName is null, cannot initiate save");
379+ throw new NullReferenceException("FileName is null, cannot initiate save");
380380
381381 if ((object)m_fileData == null)
382− throw new ArgumentNullException("FileData", "FileData is null, cannot initiate save");
382+ throw new NullReferenceException("FileData is null, cannot initiate save");
383383
384384 m_saveIsReady.Reset();
385385 ThreadPool.QueueUserWorkItem(SynchronizedWrite);
386386 }
387387
388388 /// <summary>
389− /// Initiates interprocess synchronized cache file load.
389+ /// Initiates inter-process synchronized cache file load.
390390 /// </summary>
391391 /// <remarks>
392392 /// Subclasses should always call <see cref="WaitForLoad()"/> before calling this method.
@@ -394,7 +394,7 @@ namespace GSF.IO · 1 unmodified line
394394 public virtual void Load()
395395 {
396396 if ((object)m_fileName == null)
397− throw new ArgumentNullException("FileName", "FileName is null, cannot initiate load");
397+ throw new NullReferenceException("FileName is null, cannot initiate load");
398398
399399 m_loadIsReady.Reset();
400400 ThreadPool.QueueUserWorkItem(SynchronizedRead);
@@ -471,58 +471,58 @@ namespace GSF.IO · 70 unmodified lines
471471 {
472472 try
473473 {
474− if (!m_disposed)
474+ if (m_disposed)
475+ return;
476+
477+ if (m_fileLock.TryEnterWriteLock((int)m_retryTimer.Interval))
475478 {
476− if (m_fileLock.TryEnterWriteLock((int)m_retryTimer.Interval))
479+ FileStream fileStream = null;
480+
481+ try
477482 {
478− FileStream fileStream = null;
483+ fileStream = new FileStream(m_fileName, FileMode.Create, FileAccess.Write, FileShare.None);
479484
480− try
485+ if (m_dataLock.TryEnterReadLock((int)m_retryTimer.Interval))
481486 {
482− fileStream = new FileStream(m_fileName, FileMode.Create, FileAccess.Write, FileShare.None);
483−
484− if (m_dataLock.TryEnterReadLock((int)m_retryTimer.Interval))
487+ try
485488 {
486− try
487− {
488− // Disable file watch notification before update
489− if ((object)m_fileWatcher != null)
490− m_fileWatcher.EnableRaisingEvents = false;
489+ // Disable file watch notification before update
490+ if ((object)m_fileWatcher != null)
491+ m_fileWatcher.EnableRaisingEvents = false;
491492
492− SaveFileData(fileStream, m_fileData);
493− m_saveIsReady.Set();
494− }
495− finally
496− {
497− m_dataLock.ExitReadLock();
498−
499− // Reenable file watch notification
500− if ((object)m_fileWatcher != null)
501− m_fileWatcher.EnableRaisingEvents = true;
502− }
493+ SaveFileData(fileStream, m_fileData);
494+ m_saveIsReady.Set();
503495 }
504− else
496+ finally
505497 {
506− RetrySynchronizedEvent(new TimeoutException("Timeout waiting to acquire read lock for local cache"), WriteEvent);
498+ m_dataLock.ExitReadLock();
499+
500+ // Re-enable file watch notification
501+ if ((object)m_fileWatcher != null)
502+ m_fileWatcher.EnableRaisingEvents = true;
507503 }
508504 }
509− catch (IOException ex)
510− {
511− RetrySynchronizedEvent(ex, WriteEvent);
512− }
513− finally
505+ else
514506 {
515− m_fileLock.ExitWriteLock();
516−
517− if ((object)fileStream != null)
518− fileStream.Close();
507+ RetrySynchronizedEvent(new TimeoutException("Timeout waiting to acquire read lock for local cache"), WriteEvent);
519508 }
520509 }
521− else
510+ catch (IOException ex)
522511 {
523− RetrySynchronizedEvent(new TimeoutException("Timeout waiting to acquire write lock for " + m_fileName), WriteEvent);
512+ RetrySynchronizedEvent(ex, WriteEvent);
513+ }
514+ finally
515+ {
516+ m_fileLock.ExitWriteLock();
517+
518+ if ((object)fileStream != null)
519+ fileStream.Close();
524520 }
525521 }
522+ else
523+ {
524+ RetrySynchronizedEvent(new TimeoutException("Timeout waiting to acquire write lock for " + m_fileName), WriteEvent);
525+ }
526526 }
527527 catch (ThreadAbortException)
528528 {
@@ -542,63 +542,63 @@ namespace GSF.IO · 13 unmodified lines
542542 {
543543 try
544544 {
545− if (!m_disposed)
545+ if (m_disposed)
546+ return;
547+
548+ if (File.Exists(m_fileName))
546549 {
547− if (File.Exists(m_fileName))
550+ if (m_fileLock.TryEnterReadLock((int)m_retryTimer.Interval))
548551 {
549− if (m_fileLock.TryEnterReadLock((int)m_retryTimer.Interval))
552+ FileStream fileStream = null;
553+
554+ try
550555 {
551− FileStream fileStream = null;
556+ fileStream = new FileStream(m_fileName, FileMode.Open, FileAccess.Read, FileShare.Read);
552557
553− try
558+ if (m_dataLock.TryEnterWriteLock((int)m_retryTimer.Interval))
554559 {
555− fileStream = new FileStream(m_fileName, FileMode.Open, FileAccess.Read, FileShare.Read);
556−
557− if (m_dataLock.TryEnterWriteLock((int)m_retryTimer.Interval))
560+ try
558561 {
559− try
560− {
561− m_fileData = LoadFileData(fileStream);
562− }
563− finally
564− {
565− m_dataLock.ExitWriteLock();
566− }
567−
568− // Release any threads waiting for file data
569− m_loadIsReady.Set();
562+ m_fileData = LoadFileData(fileStream);
570563 }
571− else
564+ finally
572565 {
573− RetrySynchronizedEvent(new TimeoutException("Timeout waiting to acquire write lock for local cache"), ReadEvent);
566+ m_dataLock.ExitWriteLock();
574567 }
568+
569+ // Release any threads waiting for file data
570+ m_loadIsReady.Set();
575571 }
576− catch (IOException ex)
577− {
578− RetrySynchronizedEvent(ex, ReadEvent);
579− }
580− finally
572+ else
581573 {
582− m_fileLock.ExitReadLock();
583−
584− if ((object)fileStream != null)
585− fileStream.Close();
574+ RetrySynchronizedEvent(new TimeoutException("Timeout waiting to acquire write lock for local cache"), ReadEvent);
586575 }
587576 }
588− else
577+ catch (IOException ex)
589578 {
590− RetrySynchronizedEvent(new TimeoutException("Timeout waiting to acquire read lock for " + m_fileName), ReadEvent);
579+ RetrySynchronizedEvent(ex, ReadEvent);
580+ }
581+ finally
582+ {
583+ m_fileLock.ExitReadLock();
584+
585+ if ((object)fileStream != null)
586+ fileStream.Close();
591587 }
592588 }
593589 else
594590 {
595− // File doesn't exist, create ane empty array representing a zero-length file
596− m_fileData = new byte[0];
597−
598− // Release any threads waiting for file data
599− m_loadIsReady.Set();
591+ RetrySynchronizedEvent(new TimeoutException("Timeout waiting to acquire read lock for " + m_fileName), ReadEvent);
600592 }
601593 }
594+ else
595+ {
596+ // File doesn't exist, create an empty array representing a zero-length file
597+ m_fileData = new byte[0];
598+
599+ // Release any threads waiting for file data
600+ m_loadIsReady.Set();
601+ }
602602 }
603603 catch (ThreadAbortException)
604604 {
@@ -618,34 +618,35 @@ namespace GSF.IO · 13 unmodified lines
618618 /// <param name="eventType">Event type to retry.</param>
619619 private void RetrySynchronizedEvent(Exception ex, int eventType)
620620 {
621+ if (m_disposed)
622+ return;
623+
621624 // A retry is only being initiating for basic file I/O or locking errors, all other errors will initiate an unhandled
622625 // exception causing system exit. It would be an error, IMO, for the system to create values then not be able to load
623626 // them at next run or not be able to use values from last run because file could not be loaded.
624− if (!m_disposed)
627+
628+ // We monitor basic I/O and lock failures occurring in quick succession, we can't allow retry activity to go on forever
629+ if (DateTime.UtcNow.Ticks - m_lastRetryTime > (long)Ticks.FromMilliseconds(m_retryTimer.Interval * m_maximumRetryAttempts))
625630 {
626− // We monitor basic I/O and lock failures occurring in quick succession, we can't allow retry activity to go on forever
627− if (DateTime.UtcNow.Ticks - m_lastRetryTime > (long)Ticks.FromMilliseconds(m_retryTimer.Interval * m_maximumRetryAttempts))
628− {
629− // Significant time has passed since last retry, so we reset counter
630− m_retryCount = 0;
631− m_lastRetryTime = DateTime.UtcNow.Ticks;
632− }
633− else
634− {
635− m_retryCount++;
631+ // Significant time has passed since last retry, so we reset counter
632+ m_retryCount = 0;
633+ m_lastRetryTime = DateTime.UtcNow.Ticks;
634+ }
635+ else
636+ {
637+ m_retryCount++;
636638
637− if (m_retryCount >= m_maximumRetryAttempts)
638− throw new UnauthorizedAccessException("Failed to " + (eventType == WriteEvent ? "write data to " : "read data from ") + m_fileName + " after " + m_maximumRetryAttempts + " attempts: " + ex.Message, ex);
639− }
639+ if (m_retryCount >= m_maximumRetryAttempts)
640+ throw new UnauthorizedAccessException("Failed to " + (eventType == WriteEvent ? "write data to " : "read data from ") + m_fileName + " after " + m_maximumRetryAttempts + " attempts: " + ex.Message, ex);
641+ }
640642
641− // Technically the interprocess mutex will handle serialized access to the file, but if the OS or other process
642− // not participating with the mutex has the file locked, all we can do is queue up a retry for this event.
643− lock (m_retryQueue)
644− {
645− m_retryQueue[eventType] = true;
646− }
647− m_retryTimer.Start();
643+ // Technically the inter-process mutex will handle serialized access to the file, but if the OS or other process
644+ // not participating with the mutex has the file locked, all we can do is queue up a retry for this event.
645+ lock (m_retryQueue)
646+ {
647+ m_retryQueue[eventType] = true;
648648 }
649+ m_retryTimer.Start();
649650 }
650651
651652 /// <summary>
@@ -653,34 +654,34 @@ namespace GSF.IO · 1 unmodified line
653654 /// </summary>
654655 private void m_retryTimer_Elapsed(object sender, ElapsedEventArgs e)
655656 {
656− if (!m_disposed)
657− {
658− WaitCallback callBackEvent = null;
657+ if (m_disposed)
658+ return;
659659
660− lock (m_retryQueue)
661− {
662− // Reads should always occur first since you may need to load any
663− // newly written data before saving new data. Users can override
664− // load and save behavior to "merge" data sets if needed.
665− if (m_retryQueue[ReadEvent])
666− {
667− callBackEvent = SynchronizedRead;
668− m_retryQueue[ReadEvent] = false;
669− }
670− else if (m_retryQueue[WriteEvent])
671− {
672− callBackEvent = SynchronizedWrite;
673− m_retryQueue[WriteEvent] = false;
674− }
660+ WaitCallback callBackEvent = null;
675661
676− // If any events remain queued for retry, start timer for next event
677− if (m_retryQueue.Any(true))
678− m_retryTimer.Start();
662+ lock (m_retryQueue)
663+ {
664+ // Reads should always occur first since you may need to load any
665+ // newly written data before saving new data. Users can override
666+ // load and save behavior to "merge" data sets if needed.
667+ if (m_retryQueue[ReadEvent])
668+ {
669+ callBackEvent = SynchronizedRead;
670+ m_retryQueue[ReadEvent] = false;
671+ }
672+ else if (m_retryQueue[WriteEvent])
673+ {
674+ callBackEvent = SynchronizedWrite;
675+ m_retryQueue[WriteEvent] = false;
679676 }
680677
681− if ((object)callBackEvent != null)
682− ThreadPool.QueueUserWorkItem(callBackEvent);
678+ // If any events remain queued for retry, start timer for next event
679+ if (m_retryQueue.Any(true))
680+ m_retryTimer.Start();
683681 }
682+
683+ if ((object)callBackEvent != null)
684+ ThreadPool.QueueUserWorkItem(callBackEvent);
684685 }
685686
686687 /// <summary>
Source/Libraries/GSF.Core/Security/Cryptography/Cipher.cs+3−7
@@ -428,7 +428,7 @@ namespace GSF.Security.Cryptography · 427 unmodified lines
428428 static Cipher()
429429 {
430430 const string fipsKeyOld = "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Lsa";
431− const string fipsKeyNew = "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Lsa\\FIPSAlgorithmPolicy";
431+ const string fipsKeyNew = "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Lsa\\FipsAlgorithmPolicy";
432432
433433 KeyIVCache localKeyIVCache;
434434 string localCacheFileName = DefaultCacheFileName;
@@ -447,7 +447,7 @@ namespace GSF.Security.Cryptography · 12 unmodified lines
447447 retryDelayInterval = settings["CacheRetryDelayInterval"].ValueAs(retryDelayInterval);
448448 maximumRetryAttempts = settings["CacheMaximumRetryAttempts"].ValueAs(maximumRetryAttempts);
449449
450− // Determine if the user needs to use FIPS-compliant algorithms.
450+ // Determine if the user needs to use FIPS-compliant algorithms
451451 s_managedEncryption = (Registry.GetValue(fipsKeyNew, "Enabled", 0) ?? Registry.GetValue(fipsKeyOld, "FIPSAlgorithmPolicy", 0)).ToString() == "0";
452452
453453 // Initialize local cryptographic key and initialization vector cache (application may only have read-only access to this cache)
@@ -473,8 +473,7 @@ namespace GSF.Security.Cryptography · 19 unmodified lines
473473 try
474474 {
475475 // Validate that user has write access to the local cryptographic cache folder
476−#if MONO
477− string tempFile = FilePath.GetDirectoryName(localCacheFileName) + Guid.NewGuid().ToString() + ".tmp";
476+ string tempFile = FilePath.GetDirectoryName(localCacheFileName) + Guid.NewGuid() + ".tmp";
478477
479478 using (File.Create(tempFile))
480479 {
@@ -482,9 +481,6 @@ namespace GSF.Security.Cryptography · 1 unmodified line
482481
483482 if (File.Exists(tempFile))
484483 File.Delete(tempFile);
485−#else
486− Directory.GetAccessControl(FilePath.GetDirectoryName(localCacheFileName));
487−#endif
488484
489485 // No access issues exist, use local cache as the primary cryptographic key and initialization vector cache
490486 s_keyIVCache = localKeyIVCache;
Source/Libraries/GSF.Core/Threading/InterprocessLock.cs+2−2
@@ -87,7 +87,7 @@ namespace GSF.Threading · 86 unmodified lines
8787 // Create a mutex name that is specific to an object (e.g., a path and file name).
8888 // Note that we use GetPasswordHash to create a short common name for the name parameter
8989 // that was passed into the function - this allows the parameter to be very long, e.g.,
90− // a file path, and still meet minimum semaphore name requirements.
90+ // a file path, and still meet minimum mutex name requirements.
9191
9292 // Prefix mutex name with "Global\" such that mutex will apply to all active
9393 // application sessions in case terminal services is running.
@@ -211,7 +211,7 @@ namespace GSF.Threading · 117 unmodified lines
211211 catch (UnauthorizedAccessException)
212212 {
213213 // Named semaphore exists now but current user doesn't have full control, attempt to open with minimum needed rights
214− namedSemaphore = Semaphore.OpenExisting(name, SemaphoreRights.Synchronize | SemaphoreRights.Modify);
214+ namedSemaphore = Semaphore.OpenExisting(semaphoreName, SemaphoreRights.Synchronize | SemaphoreRights.Modify);
215215 }
216216 }
217217 #endif
Source/Libraries/GSF.Core/Threading/InterprocessReaderWriterLock.cs+24−16
@@ -29,7 +29,7 @@ using System.Threading; · 28 unmodified lines
2929 namespace GSF.Threading
3030 {
3131 /// <summary>
32− /// Represents an interprocess reader/writer lock using <see cref="Semaphore"/> and <see cref="Mutex"/> native locking mechanisms.
32+ /// Represents an inter-process reader/writer lock using <see cref="Semaphore"/> and <see cref="Mutex"/> native locking mechanisms.
3333 /// </summary>
3434 public class InterprocessReaderWriterLock : IDisposable
3535 {
@@ -43,9 +43,9 @@ namespace GSF.Threading · 7 unmodified lines
4343 public const int DefaultMaximumConcurrentLocks = 10;
4444
4545 // Fields
46− private Mutex m_semaphoreLock; // Mutex used to synchronize access to Semaphore
47− private Semaphore m_concurrencyLock; // Semaphore used for reader/writer lock on consumer object
48− private readonly int m_maximumConcurrentLocks;
46+ private Mutex m_semaphoreLock; // Mutex used to synchronize access to Semaphore
47+ private Semaphore m_concurrencyLock; // Semaphore used for reader/writer lock on consumer object
48+ private readonly int m_maximumConcurrentLocks; // Maximum number of concurrent locks before waiting
4949 private bool m_disposed;
5050
5151 #endregion
@@ -56,7 +56,7 @@ namespace GSF.Threading · 4 unmodified lines
5656 /// Creates a new instance of the <see cref="InterprocessReaderWriterLock"/> associated with the specified
5757 /// <paramref name="name"/> that identifies a source object needing concurrency locking.
5858 /// </summary>
59− /// <param name="name">Identifiying name of source object needing concurrency locking (e.g., a path and file name).</param>
59+ /// <param name="name">Identifying name of source object needing concurrency locking (e.g., a path and file name).</param>
6060 public InterprocessReaderWriterLock(string name)
6161 : this(name, DefaultMaximumConcurrentLocks)
6262 {
@@ -66,7 +66,7 @@ namespace GSF.Threading · 3 unmodified lines
6666 /// Creates a new instance of the <see cref="InterprocessReaderWriterLock"/> associated with the specified
6767 /// <paramref name="name"/> that identifies a source object needing concurrency locking.
6868 /// </summary>
69− /// <param name="name">Identifiying name of source object needing concurrency locking (e.g., a path and file name).</param>
69+ /// <param name="name">Identifying name of source object needing concurrency locking (e.g., a path and file name).</param>
7070 /// <param name="maximumConcurrentLocks">Maximum concurrent reader locks to allow.</param>
7171 /// <remarks>
7272 /// If more reader locks are requested than the <paramref name="maximumConcurrentLocks"/>, excess reader locks will simply
@@ -128,12 +128,16 @@ namespace GSF.Threading · 55 unmodified lines
128128 if (disposing)
129129 {
130130 if ((object)m_concurrencyLock != null)
131+ {
131132 m_concurrencyLock.Close();
132− m_concurrencyLock = null;
133+ m_concurrencyLock = null;
134+ }
133135
134136 if ((object)m_semaphoreLock != null)
137+ {
135138 m_semaphoreLock.Close();
136− m_semaphoreLock = null;
139+ m_semaphoreLock = null;
140+ }
137141 }
138142 }
139143 finally
@@ -147,7 +151,7 @@ namespace GSF.Threading · 7 unmodified lines
147151 /// Tries to enter the lock in read mode.
148152 /// </summary>
149153 /// <remarks>
150− /// Upon successful aquistion of a read lock, use the <c>finally</c> block of a <c>try/finally</c> statement to call <see cref="ExitReadLock"/>.
154+ /// Upon successful acquisition of a read lock, use the <c>finally</c> block of a <c>try/finally</c> statement to call <see cref="ExitReadLock"/>.
151155 /// One <see cref="ExitReadLock"/> should be called for each <see cref="EnterReadLock"/> or <see cref="TryEnterReadLock"/>.
152156 /// </remarks>
153157 public void EnterReadLock()
@@ -159,7 +163,7 @@ namespace GSF.Threading · 5 unmodified lines
159163 /// Tries to enter the lock in write mode.
160164 /// </summary>
161165 /// <remarks>
162− /// Upon successful aquistion of a write lock, use the <c>finally</c> block of a <c>try/finally</c> statement to call <see cref="ExitWriteLock"/>.
166+ /// Upon successful acquisition of a write lock, use the <c>finally</c> block of a <c>try/finally</c> statement to call <see cref="ExitWriteLock"/>.
163167 /// One <see cref="ExitWriteLock"/> should be called for each <see cref="EnterWriteLock"/> or <see cref="TryEnterWriteLock"/>.
164168 /// </remarks>
165169 public void EnterWriteLock()
@@ -171,7 +175,7 @@ namespace GSF.Threading · 5 unmodified lines
171175 /// Exits read mode and returns the prior read lock count.
172176 /// </summary>
173177 /// <remarks>
174− /// Upon successful aquistion of a read lock, use the <c>finally</c> block of a <c>try/finally</c> statement to call <see cref="ExitReadLock"/>.
178+ /// Upon successful acquisition of a read lock, use the <c>finally</c> block of a <c>try/finally</c> statement to call <see cref="ExitReadLock"/>.
175179 /// One <see cref="ExitReadLock"/> should be called for each <see cref="EnterReadLock"/> or <see cref="TryEnterReadLock"/>.
176180 /// </remarks>
177181 public int ExitReadLock()
@@ -184,7 +188,7 @@ namespace GSF.Threading · 6 unmodified lines
184188 /// Exits write mode.
185189 /// </summary>
186190 /// <remarks>
187− /// Upon successful aquistion of a write lock, use the <c>finally</c> block of a <c>try/finally</c> statement to call <see cref="ExitWriteLock"/>.
191+ /// Upon successful acquisition of a write lock, use the <c>finally</c> block of a <c>try/finally</c> statement to call <see cref="ExitWriteLock"/>.
188192 /// One <see cref="ExitWriteLock"/> should be called for each <see cref="EnterWriteLock"/> or <see cref="TryEnterWriteLock"/>.
189193 /// </remarks>
190194 public void ExitWriteLock()
@@ -199,12 +203,12 @@ namespace GSF.Threading · 8 unmodified lines
199203 /// <param name="millisecondsTimeout">The number of milliseconds to wait, or -1 (<see cref="Timeout.Infinite"/>) to wait indefinitely.</param>
200204 /// <returns><c>true</c> if the calling thread entered read mode, otherwise, <c>false</c>.</returns>
201205 /// <remarks>
202− /// Upon successful aquistion of a read lock, use the <c>finally</c> block of a <c>try/finally</c> statement to call <see cref="ExitReadLock"/>.
206+ /// Upon successful acquisition of a read lock, use the <c>finally</c> block of a <c>try/finally</c> statement to call <see cref="ExitReadLock"/>.
203207 /// One <see cref="ExitReadLock"/> should be called for each <see cref="EnterReadLock"/> or <see cref="TryEnterReadLock"/>.
204208 /// </remarks>
205209 public bool TryEnterReadLock(int millisecondsTimeout)
206210 {
207− bool success = false;
211+ bool success;
208212
209213 try
210214 {
@@ -263,7 +267,9 @@ namespace GSF.Threading · 52 unmodified lines
263267 // The only way to get a semaphore slot count is to execute a successful wait and release.
264268 startTime = DateTime.UtcNow.Ticks;
265269
266− if (success = m_concurrencyLock.WaitOne(millisecondsTimeout))
270+ success = m_concurrencyLock.WaitOne(millisecondsTimeout);
271+
272+ if (success)
267273 {
268274 int count = m_concurrencyLock.Release();
269275 int adjustedTimeout = millisecondsTimeout;
@@ -282,7 +288,9 @@ namespace GSF.Threading · 12 unmodified lines
282288 if (adjustedTimeout < 0)
283289 adjustedTimeout = 0;
284290
285− if (success = m_concurrencyLock.WaitOne(adjustedTimeout))
291+ success = m_concurrencyLock.WaitOne(adjustedTimeout);
292+
293+ if (success)
286294 count = m_concurrencyLock.Release();
287295 }
288296 }
Source/Libraries/GSF.Security/AdoSecurityCache.cs+296−0
@@ -0,0 +1,296 @@
1+//******************************************************************************************************
2+// AdoSecurityCache.cs - Gbtc
3+//
4+// Copyright © 2013, 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+// 12/30/2013 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//******************************************************************************************************
23+
24+using System;
25+using System.Data;
26+using System.IO;
27+using System.Security.Cryptography;
28+using GSF.Configuration;
29+using GSF.Data;
30+using GSF.IO;
31+using GSF.Threading;
32+
33+namespace GSF.Security
34+{
35+ /// <summary>
36+ /// Represents a secured inter-process cache for the security context needed by the <see cref="AdoSecurityProvider"/>.
37+ /// </summary>
38+ /// <remarks>
39+ /// This is a system cache that contains the last known security information loaded from the database related to users,
40+ /// roles and groups. This cache allows the <see cref="AdoSecurityProvider"/> to start-up without database access
41+ /// using latest cached security context. Even though this cache contains role information for all users, it does
42+ /// not overlap information in the <see cref="UserRoleCache"/> since the user role cache contains only the highest
43+ /// order role assignment for a user at last login and is used for auditing.
44+ /// </remarks>
45+ public class AdoSecurityCache : InterprocessCache
46+ {
47+ #region [ Members ]
48+
49+ // Constants
50+
51+ // Default ADO security cache file name
52+ private const string DefaultCacheFileName = "AdoSecurityCache.bin";
53+
54+ // Fields
55+ private DataSet m_dataSet; // Internal ADO security data set
56+ private readonly object m_dataSetLock; // Lock object for internal data set
57+
58+ #endregion
59+
60+ /// <summary>
61+ /// Creates a new instance of the <see cref="AdoSecurityCache"/> with the specified number of <paramref name="maximumConcurrentLocks"/>.
62+ /// </summary>
63+ /// <param name="maximumConcurrentLocks">Maximum concurrent reader locks to allow.</param>
64+ public AdoSecurityCache(int maximumConcurrentLocks = InterprocessReaderWriterLock.DefaultMaximumConcurrentLocks)
65+ : base(maximumConcurrentLocks)
66+ {
67+ m_dataSet = new DataSet("AdoSecurityContext");
68+ m_dataSetLock = new object();
69+ }
70+
71+ #region [ Properties ]
72+
73+ /// <summary>
74+ /// Gets or sets the internal <see cref="DataSet"/>; returned value will be a copy of the internal.
75+ /// </summary>
76+ public DataSet DataSet
77+ {
78+ get
79+ {
80+ DataSet dataSet;
81+
82+ // We wait until the data set cache is loaded before attempting to access it
83+ WaitForDataReady();
84+
85+ // Wait for thread level lock on data set
86+ lock (m_dataSetLock)
87+ {
88+ // Since user could changes, make a copy of the data set for external use
89+ dataSet = m_dataSet.Copy();
90+ }
91+
92+ return dataSet;
93+ }
94+ set
95+ {
96+ // Wait for thread level lock on data set
97+ lock (m_dataSetLock)
98+ {
99+ // Assign new data set
100+ m_dataSet = value;
101+ }
102+
103+ // Queue up a serialization for this new data set
104+ Save();
105+ }
106+ }
107+
108+ #endregion
109+
110+ #region [ Methods ]
111+
112+ /// <summary>
113+ /// Initiates inter-process synchronized save of <see cref="DataSet"/>.
114+ /// </summary>
115+ public override void Save()
116+ {
117+ byte[] serializedDataSet;
118+
119+ // Wait for thread level lock on data set
120+ lock (m_dataSetLock)
121+ {
122+ using (BlockAllocatedMemoryStream stream = new BlockAllocatedMemoryStream())
123+ {
124+ m_dataSet.SerializeToStream(stream);
125+ serializedDataSet = stream.ToArray();
126+ }
127+ }
128+
129+ // File data is the serialized data set, assignment will initiate auto-save if needed
130+ FileData = serializedDataSet;
131+ }
132+
133+ /// <summary>
134+ /// Handles serialization of file to disk; virtual method allows customization (e.g., pre-save encryption and/or data merge).
135+ /// </summary>
136+ /// <param name="fileStream"><see cref="FileStream"/> used to serialize data.</param>
137+ /// <param name="fileData">File data to be serialized.</param>
138+ /// <remarks>
139+ /// Consumers overriding this method should not directly call <see cref="InterprocessCache.FileData"/> property to avoid potential dead-locks.
140+ /// </remarks>
141+ protected override void SaveFileData(FileStream fileStream, byte[] fileData)
142+ {
143+ // Encrypt data local to this machine (this way user cannot copy ADO security cache to another machine)
144+ base.SaveFileData(fileStream, ProtectedData.Protect(fileData, null, DataProtectionScope.LocalMachine));
145+ }
146+
147+ /// <summary>
148+ /// Handles deserialization of file from disk; virtual method allows customization (e.g., pre-load decryption and/or data merge).
149+ /// </summary>
150+ /// <param name="fileStream"><see cref="FileStream"/> used to deserialize data.</param>
151+ /// <returns>Deserialized file data.</returns>
152+ /// <remarks>
153+ /// Consumers overriding this method should not directly call <see cref="InterprocessCache.FileData"/> property to avoid potential dead-locks.
154+ /// </remarks>
155+ protected override byte[] LoadFileData(FileStream fileStream)
156+ {
157+ // Decrypt data that was encrypted local to this machine
158+ byte[] serializedDataSet = ProtectedData.Unprotect(fileStream.ReadStream(), null, DataProtectionScope.LocalMachine);
159+ DataSet dataSet;
160+
161+ using (MemoryStream stream = new MemoryStream(serializedDataSet))
162+ {
163+ dataSet = stream.DeserializeToDataSet();
164+ }
165+
166+ // Wait for thread level lock on data set
167+ lock (m_dataSetLock)
168+ {
169+ m_dataSet = dataSet;
170+ }
171+
172+ return serializedDataSet;
173+ }
174+
175+ // Waits until the cache is loaded before attempting to access it
176+ private void WaitForDataReady()
177+ {
178+ try
179+ {
180+ // Just wrapping this method to provide a more detailed exception message if there is an issue loading cache
181+ WaitForLoad();
182+ }
183+ catch (Exception ex)
184+ {
185+ throw new UnauthorizedAccessException("ADO security cache access failure: timeout while attempting to load last valid security context.", ex);
186+ }
187+ }
188+
189+ #endregion
190+
191+ #region [ Static ]
192+
193+ // Static Methods
194+
195+ /// <summary>
196+ /// Loads the <see cref="AdoSecurityCache"/> for the current local user.
197+ /// </summary>
198+ /// <returns>Loaded instance of the <see cref="AdoSecurityCache"/>.</returns>
199+ public static AdoSecurityCache GetCurrentCache()
200+ {
201+ AdoSecurityCache currentCache;
202+ AdoSecurityCache localSecurityCache;
203+
204+ // Define default cache path
205+ string cachePath = string.Format("{0}\\ConfigurationCache\\", FilePath.GetAbsolutePath(""));
206+
207+ // Make sure configuration cache path setting exists
208+ ConfigurationFile configFile = ConfigurationFile.Current;
209+ CategorizedSettingsElementCollection systemSettings = configFile.Settings["systemSettings"];
210+ systemSettings.Add("ConfigurationCachePath", cachePath, "Defines the path used to cache serialized configurations");
211+
212+ // Retrieve configuration cache path as defined in the config file
213+ cachePath = systemSettings["ConfigurationCachePath"].Value;
214+
215+ // Make sure configuration cache path exists
216+ if (!Directory.Exists(cachePath))
217+ Directory.CreateDirectory(cachePath);
218+
219+ string localCacheFileName = Path.Combine(cachePath, DefaultCacheFileName);
220+
221+ // Initialize local ADO security cache (application may only have read-only access to this cache)
222+ localSecurityCache = new AdoSecurityCache
223+ {
224+ FileName = localCacheFileName,
225+#if DNF45
226+ ReloadOnChange = true,
227+#else
228+ // Reload on change is disabled to eliminate GC handle leaks on .NET 4.0, this prevents
229+ // automatic runtime reloading of key/iv data cached by another application.
230+ ReloadOnChange = false,
231+#endif
232+ AutoSave = false
233+ };
234+
235+ // Load initial ADO security data set
236+ localSecurityCache.Load();
237+
238+ try
239+ {
240+ // Validate that current user has write access to the local cache folder
241+ string tempFile = FilePath.GetDirectoryName(localCacheFileName) + Guid.NewGuid() + ".tmp";
242+
243+ using (File.Create(tempFile))
244+ {
245+ }
246+
247+ if (File.Exists(tempFile))
248+ File.Delete(tempFile);
249+
250+ // No access issues exist, use local cache as the primary cache
251+ currentCache = localSecurityCache;
252+ currentCache.AutoSave = true;
253+ }
254+ catch (UnauthorizedAccessException)
255+ {
256+ // User does not have needed serialization access to common cache folder,
257+ // use a path where user will have rights
258+ string userCacheFolder = FilePath.AddPathSuffix(FilePath.GetApplicationDataFolder());
259+ string userCacheFileName = userCacheFolder + FilePath.GetFileName(localCacheFileName);
260+
261+ // Make sure user directory exists
262+ if (!Directory.Exists(userCacheFolder))
263+ Directory.CreateDirectory(userCacheFolder);
264+
265+ // Copy existing common cache if none exists
266+ if (File.Exists(localCacheFileName) && !File.Exists(userCacheFileName))
267+ File.Copy(localCacheFileName, userCacheFileName);
268+
269+ // Initialize primary cache within user folder
270+ currentCache = new AdoSecurityCache
271+ {
272+ FileName = userCacheFileName,
273+#if DNF45
274+ ReloadOnChange = true,
275+#else
276+ // Reload on change is disabled to eliminate GC handle leaks on .NET 4.0, this prevents
277+ // automatic runtime reloading of key/iv data cached by another application.
278+ ReloadOnChange = false,
279+#endif
280+ AutoSave = true
281+ };
282+
283+ // Load initial ADO security data set
284+ currentCache.Load();
285+
286+ // Update user located security cache if locally located security cache is newer
287+ if (File.GetLastWriteTime(localCacheFileName) > File.GetLastWriteTime(userCacheFileName))
288+ currentCache.DataSet = localSecurityCache.DataSet;
289+ }
290+
291+ return currentCache;
292+ }
293+
294+ #endregion
295+ }
296+}
Source/Libraries/GSF.Security/AdoSecurityProvider.cs+362−157
@@ -27,10 +27,14 @@ · 26 unmodified lines
2727 // Modified database queries to work with Oracle database.
2828 // 12/20/2012 - Starlynn Danyelle Gilliam
2929 // Modified Header.
30+// 01/16/2014 - J. Ritchie Carroll
31+// Added security context caching so security system can load without database access
32+// using last cached security state.
3033 //
3134 //******************************************************************************************************
3235
3336 using System;
37+using System.Collections.Generic;
3438 using System.Configuration;
3539 using System.Data;
3640 using System.Diagnostics;
@@ -39,17 +43,19 @@ using System.Security; · 2 unmodified lines
3943 using System.Text.RegularExpressions;
4044 using System.Threading;
4145 using System.Web.Security;
46+using GSF.Collections;
4247 using GSF.Configuration;
4348 using GSF.Data;
49+using GSF.Identity;
4450
4551 namespace GSF.Security
4652 {
4753 /// <summary>
48− /// Represents an <see cref="ISecurityProvider"/> that uses ADO.NET data source (SQL Server, MySQL, Microsoft Access etc) for its
49− /// backend datastore and authenticates internal users against Active Directory and external users against the database.
54+ /// Represents an <see cref="ISecurityProvider"/> that uses ADO.NET data source (SQL Server, MySQL, Oracle, etc.) for its
55+ /// backend data store and authenticates internal users against Active Directory and external users against the database.
5056 /// </summary>
5157 /// <example>
52− /// Required config file entries:
58+ /// Required config file entries (automatically added):
5359 /// <code>
5460 /// <![CDATA[
5561 /// <?xml version="1.0"?>
@@ -88,14 +94,84 @@ namespace GSF.Security · 32 unmodified lines
8894 /// ]]>
8995 /// </code>
9096 /// </example>
97+ /// <remarks>
98+ /// Minimum expected table schema for ADO Security Provider:
99+ /// <code>
100+ /// <![CDATA[
101+ /// CREATE TABLE UserAccount
102+ /// (
103+ /// ID UNIQUEINDENTIFIER NOT NULL DEFAULT NEWID(),
104+ /// Name VARCHAR(200) NOT NULL,
105+ /// Password VARCHAR(200) DEFAULT NULL,
106+ /// FirstName VARCHAR(200) DEFAULT NULL,
107+ /// LastName VARCHAR(200) DEFAULT NULL,
108+ /// Phone VARCHAR(200) DEFAULT NULL,
109+ /// Email VARCHAR(200) DEFAULT NULL,
110+ /// LockedOut TINYINT NOT NULL DEFAULT 0,
111+ /// UseADAuthentication TINYINT NOT NULL DEFAULT 1,
112+ /// ChangePasswordOn DATETIME DEFAULT NULL,
113+ /// CONSTRAINT PK_UserAccount PRIMARY KEY (ID ASC),
114+ /// CONSTRAINT IX_UserAccount UNIQUE KEY (Name)
115+ /// );
116+ ///
117+ /// CREATE TABLE SecurityGroup
118+ /// (
119+ /// ID UNIQUEINDENTIFIER NOT NULL DEFAULT NEWID(),
120+ /// Name VARCHAR(200) NOT NULL,
121+ /// CONSTRAINT PK_SecurityGroup PRIMARY KEY (ID ASC),
122+ /// CONSTRAINT IX_SecurityGroup UNIQUE KEY (Name)
123+ /// );
124+ ///
125+ /// CREATE TABLE SecurityGroupUserAccount
126+ /// (
127+ /// SecurityGroupID UNIQUEINDENTIFIER NOT NULL,
128+ /// UserAccountID UNIQUEINDENTIFIER NOT NULL
129+ /// );
130+ ///
131+ /// CREATE TABLE ApplicationRole
132+ /// (
133+ /// ID UNIQUEINDENTIFIER NOT NULL DEFAULT NEWID(),
134+ /// Name VARCHAR(200) NOT NULL,
135+ /// NodeID UNIQUEINDENTIFIER NOT NULL,
136+ /// CONSTRAINT PK_ApplicationRole PRIMARY KEY (ID ASC),
137+ /// CONSTRAINT IX_ApplicationRole UNIQUE KEY (NodeID, Name)
138+ /// );
139+ ///
140+ /// CREATE TABLE ApplicationRoleUserAccount
141+ /// (
142+ /// ApplicationRoleID UNIQUEINDENTIFIER NOT NULL,
143+ /// UserAccountID UNIQUEINDENTIFIER NOT NULL
144+ /// );
145+ ///
146+ /// CREATE TABLE ApplicationRoleSecurityGroup
147+ /// (
148+ /// ApplicationRoleID UNIQUEINDENTIFIER NOT NULL,
149+ /// SecurityGroupID UNIQUEINDENTIFIER NOT NULL
150+ /// );
151+ /// ]]>
152+ /// </code>
153+ /// </remarks>
91154 public class AdoSecurityProvider : LdapSecurityProvider
92155 {
93156 #region [ Members ]
94157
95158 // Constants
96− private const int MinimumPasswordLength = 8;
97− private const string PasswordRequirementRegex = "^.*(?=.{8,})(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).*$";
98− private const string PasswordRequirementError = "Invalid Password: Password must be at least 8 characters; must contain at least 1 number, 1 upper case letter, and 1 lower case letter";
159+ private const string UserAccountTable = "UserAccount"; // Table name for user accounts
160+ private const string SecurityGroupTable = "SecurityGroup"; // Table name for security groups
161+ private const string SecurityGroupUserAccountTable = "SecurityGroupUserAccount"; // Table name for security group user accounts
162+ private const string ApplicationRoleTable = "ApplicationRole"; // Table name for application roles
163+ private const string ApplicationRoleUserAccountTable = "ApplicationRoleUserAccount"; // Table name for application role assignments for user accounts
164+ private const string ApplicationRoleSecurityGroupTable = "ApplicationRoleSecurityGroup"; // Table name for application role assignments for security groups
165+
166+ /// <summary>
167+ /// Default regular expression used to validate new database user passwords.
168+ /// </summary>
169+ public const string DefaultPasswordRequirementsRegex = "^.*(?=.{8,})(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).*$";
170+
171+ /// <summary>
172+ /// Default error message displayed when databases users fail regular expression test.
173+ /// </summary>
174+ public const string DefaultPasswordRequirementsError = "Invalid Password: Password must be at least 8 characters; must contain at least 1 number, 1 upper case letter, and 1 lower case letter";
99175
100176 /// <summary>
101177 /// Defines the provider ID for the <see cref="AdoSecurityProvider"/>.
@@ -104,6 +180,8 @@ namespace GSF.Security · 2 unmodified lines
104180
105181 private Exception m_lastException;
106182 private bool m_successfulPassThroughAuthentication;
183+ private string m_passwordRequirementsRegex;
184+ private string m_passwordRequirementsError;
107185
108186 #endregion
109187
@@ -122,14 +200,17 @@ namespace GSF.Security · 12 unmodified lines
122200 /// Initializes a new instance of the <see cref="AdoSecurityProvider"/> class.
123201 /// </summary>
124202 /// <param name="username">Name that uniquely identifies the user.</param>
125− /// <param name="canRefreshData">true if the security provider can refresh <see cref="UserData"/> from the backend datastore, otherwise false.</param>
126− /// <param name="canUpdateData">true if the security provider can update <see cref="UserData"/> in the backend datastore, otherwise false.</param>
203+ /// <param name="canRefreshData">true if the security provider can refresh <see cref="UserData"/> from the backend data store, otherwise false.</param>
204+ /// <param name="canUpdateData">true if the security provider can update <see cref="UserData"/> in the backend data store, otherwise false.</param>
127205 /// <param name="canResetPassword">true if the security provider can reset user password, otherwise false.</param>
128206 /// <param name="canChangePassword">true if the security provider can change user password, otherwise false.</param>
129207 protected AdoSecurityProvider(string username, bool canRefreshData, bool canUpdateData, bool canResetPassword, bool canChangePassword)
130208 : base(username, canRefreshData, canUpdateData, canResetPassword, canChangePassword)
131209 {
132210 base.ConnectionString = "Eval(systemSettings.ConnectionString)";
211+
212+ m_passwordRequirementsRegex = DefaultPasswordRequirementsRegex;
213+ m_passwordRequirementsError = DefaultPasswordRequirementsError;
133214 }
134215
135216 #endregion
@@ -137,22 +218,18 @@ namespace GSF.Security · 1 unmodified line
137218 #region [ Properties ]
138219
139220 /// <summary>
140− /// Geta a boolean value that indicates whether <see cref="SecurityProviderBase.UpdateData"/> operation is supported.
221+ /// Gets a boolean value that indicates whether <see cref="SecurityProviderBase.UpdateData"/> operation is supported.
141222 /// </summary>
142223 public override bool CanUpdateData
143224 {
144225 get
145226 {
227+ // Data update supported on external user accounts.
146228 if (UserData.IsDefined && UserData.IsExternal)
147− {
148− // Data update supported on external user accounts.
149229 return true;
150− }
151− else
152− {
153− // Data update not supported on internal user accounts.
154− return false;
155− }
230+
231+ // Data update not supported on internal user accounts.
232+ return false;
156233 }
157234 }
158235
@@ -186,8 +263,14 @@ namespace GSF.Security · 27 unmodified lines
186263 // Make sure default settings exist
187264 ConfigurationFile config = ConfigurationFile.Current;
188265 CategorizedSettingsElementCollection settings = config.Settings[SettingsCategory];
189− settings.Add("DataProviderString", "Eval(systemSettings.DataProviderString)", "Configuration database ADO.NET data provider assembly type creation string to be used for connection to the backend security datastore.");
266+
267+ settings.Add("DataProviderString", "Eval(systemSettings.DataProviderString)", "Configuration database ADO.NET data provider assembly type creation string to be used for connection to the backend security data store.");
190268 settings.Add("LdapPath", "", "Specifies the LDAP path used to initialize the security provider.");
269+ settings.Add("PasswordRequirementsRegex", DefaultPasswordRequirementsRegex, "Regular expression used to validate new passwords for database users.");
270+ settings.Add("PasswordRequirementsError", DefaultPasswordRequirementsError, "Error message to be displayed when new database user password fails regular expression test.");
271+
272+ m_passwordRequirementsRegex = settings["PasswordRequirementsRegex"].ValueAs(m_passwordRequirementsRegex);
273+ m_passwordRequirementsError = settings["PasswordRequirementsError"].ValueAs(m_passwordRequirementsError);
191274 }
192275
193276 /// <summary>
@@ -208,152 +291,210 @@ namespace GSF.Security · 14 unmodified lines
208291 // Table1: Information about the user.
209292 // Table2: Groups the user is a member of.
210293 // Table3: Roles that are assigned to the user either implicitly (NT groups) or explicitly (database) or through a group.
211− AdoDataConnection database = new AdoDataConnection(SettingsCategory);
212− DataTable userDataTable = new DataTable();
213− DataTable userGroupDataTable = new DataTable();
214− DataTable userRoleDataTable = new DataTable();
215− DataRow userDataRow = null;
216− string groupName, roleName;
217− string query;
218−
219− using (IDbConnection dbConnection = database.Connection)
220− {
221− if (dbConnection == null)
222− return false;
223294
224− query = database.ParameterizedQueryString("SELECT ID, Name, Password, FirstName, LastName, Phone, Email, LockedOut, UseADAuthentication, ChangePasswordOn, CreatedOn FROM UserAccount WHERE Name = {0}", "name");
295+ DataSet securityContext;
225296
226− using (IDataReader reader = dbConnection.ExecuteReader(query, UserData.Username))
297+ try
298+ {
299+ // Attempt to extract current security context from the database
300+ using (AdoDataConnection database = new AdoDataConnection(SettingsCategory))
227301 {
228− userDataTable.Load(reader);
302+ securityContext = ExtractSecurityContext(database.Connection);
229303 }
304+ }
305+ catch (InvalidOperationException)
306+ {
307+ // Failed to open ADO connection, attempt to fall back on using security context from last known cached information
308+ securityContext = AdoSecurityCache.GetCurrentCache().DataSet;
309+ }
230310
231− if (userDataTable.Rows.Count <= 0)
232− {
233− // User doesn't exist in the database, however, user may exist in an NT authentication group which may have an explict role assignment. To test for this case
234− // we make the assumption that this is a Windows authenticated user and test for rights within groups
235− UserData.IsDefined = true;
236− UserData.IsExternal = false;
237− }
238− else
239− {
240− userDataRow = userDataTable.Rows[0];
241− UserData.IsDefined = true;
242− UserData.IsExternal = !Convert.ToBoolean(userDataRow["UseADAuthentication"]);
243− }
311+ // Validate that needed security tables exist in the data set
312+ const string SecurityContextError = "Failed to load a valid security context. Cannot proceed with user authentication.";
244313
245− if (UserData.IsExternal && (object)userDataRow != null)
246− {
247− if (string.IsNullOrEmpty(UserData.LoginID))
248− UserData.LoginID = UserData.Username;
314+ if ((object)securityContext == null)
315+ throw new SecurityException(SecurityContextError);
249316
250− if (!Convert.IsDBNull(userDataRow["Password"]))
251− UserData.Password = Convert.ToString(userDataRow["Password"]);
317+ foreach (string securityTable in s_securityTables)
318+ {
319+ if (!securityContext.Tables.Contains(securityTable))
320+ throw new SecurityException(SecurityContextError);
321+ }
252322
253− if (!Convert.IsDBNull(userDataRow["FirstName"]))
254− UserData.FirstName = Convert.ToString(userDataRow["FirstName"]);
323+ DataRow userAccount = null;
324+ Guid userAccountID = Guid.Empty;
325+ string userSID = UserInfo.AccountNameToSID(UserData.Username);
255326
256− if (!Convert.IsDBNull(userDataRow["LastName"]))
257− UserData.LastName = Convert.ToString(userDataRow["LastName"]);
327+ // Filter user account data for the current user
328+ DataRow[] userAccounts = securityContext.Tables[UserAccountTable].Select(string.Format("Name = '{0}'", userSID));
258329
259− if (!Convert.IsDBNull(userDataRow["Phone"]))
260− UserData.PhoneNumber = Convert.ToString(userDataRow["Phone"]);
330+ if (userAccounts.Length == 0)
331+ {
332+ // User doesn't exist in the database, however, user may exist in an NT authentication group which
333+ // may have an explicit role assignment. To test for this case we make the assumption that this is
334+ // a Windows authenticated user and test for rights within groups
335+ UserData.IsDefined = true;
336+ UserData.IsExternal = false;
337+ }
338+ else
339+ {
340+ userAccount = userAccounts[0];
341+ UserData.IsDefined = true;
342+ UserData.IsExternal = !Convert.ToBoolean(userAccount["UseADAuthentication"]);
343+ userAccountID = Guid.Parse(Convert.ToString(userAccount["ID"]));
344+ }
261345
262− if (!Convert.IsDBNull(userDataRow["Email"]))
263− UserData.EmailAddress = Convert.ToString(userDataRow["Email"]);
346+ if (UserData.IsExternal && (object)userAccount != null)
347+ {
348+ // Load database user details
349+ if (string.IsNullOrEmpty(UserData.LoginID))
350+ UserData.LoginID = UserData.Username;
264351
265− if (!Convert.IsDBNull(userDataRow["ChangePasswordOn"]))
266− UserData.PasswordChangeDateTime = Convert.ToDateTime(userDataRow["ChangePasswordOn"]);
352+ if (!Convert.IsDBNull(userAccount["Password"]))
353+ UserData.Password = Convert.ToString(userAccount["Password"]);
267354
268− if (!Convert.IsDBNull(userDataRow["CreatedOn"]))
269− UserData.AccountCreatedDateTime = Convert.ToDateTime(userDataRow["CreatedOn"]);
355+ if (!Convert.IsDBNull(userAccount["FirstName"]))
356+ UserData.FirstName = Convert.ToString(userAccount["FirstName"]);
270357
271− // For possible future use:
272− //if (!Convert.IsDBNull(userDataRow["UserCompanyName"]))
273− // UserData.CompanyName = Convert.ToString(userDataRow["UserCompanyName"]);
274− //if (!Convert.IsDBNull(userDataRow["UserSecurityQuestion"]))
275− // UserData.SecurityQuestion = Convert.ToString(userDataRow["UserSecurityQuestion"]);
276− //if (!Convert.IsDBNull(userDataRow["UserSecurityAnswer"]))
277− // UserData.SecurityAnswer = Convert.ToString(userDataRow["UserSecurityAnswer"]);
278− }
279− else
280− {
281− // Load implicitly assigned groups - this happens via NT user groups that get loaded into user data
282− // group collection. When group definitions are defined with the same name as their NT equivalents,
283− // this will allow automatic external group management from within NT group management (AD or local).
284− base.RefreshData(UserData.Groups, ProviderID);
285− }
358+ if (!Convert.IsDBNull(userAccount["LastName"]))
359+ UserData.LastName = Convert.ToString(userAccount["LastName"]);
286360
287− // Administrator can lock out NT user as well as database-only user via database
288− if (!UserData.IsLockedOut && userDataRow != null && !Convert.IsDBNull(userDataRow["LockedOut"]))
289− UserData.IsLockedOut = Convert.ToBoolean(userDataRow["LockedOut"]);
361+ if (!Convert.IsDBNull(userAccount["Phone"]))
362+ UserData.PhoneNumber = Convert.ToString(userAccount["Phone"]);
290363
291− // Load explicitly assigned groups
292− query = database.ParameterizedQueryString("SELECT SecurityGroupID, SecurityGroupName, SecurityGroupDescription FROM SecurityGroupUserAccountDetail WHERE UserName = {0}", "name");
364+ if (!Convert.IsDBNull(userAccount["Email"]))
365+ UserData.EmailAddress = Convert.ToString(userAccount["Email"]);
293366
294− using (IDataReader reader = dbConnection.ExecuteReader(query, UserData.Username))
295− {
296− userGroupDataTable.Load(reader);
297− }
367+ if (!Convert.IsDBNull(userAccount["ChangePasswordOn"]))
368+ UserData.PasswordChangeDateTime = Convert.ToDateTime(userAccount["ChangePasswordOn"]);
298369
299− foreach (DataRow group in userGroupDataTable.Rows)
300− {
301− if (!Convert.IsDBNull(group["SecurityGroupName"]))
302− {
303− groupName = Convert.ToString(group["SecurityGroupName"]);
370+ if (!Convert.IsDBNull(userAccount["CreatedOn"]))
371+ UserData.AccountCreatedDateTime = Convert.ToDateTime(userAccount["CreatedOn"]);
304372
305− if (!UserData.Groups.Contains(groupName, StringComparer.InvariantCultureIgnoreCase))
306− UserData.Groups.Add(groupName);
307− }
308− }
373+ // For possible future use:
374+ //if (!Convert.IsDBNull(userDataRow["UserCompanyName"]))
375+ // UserData.CompanyName = Convert.ToString(userDataRow["UserCompanyName"]);
376+ //if (!Convert.IsDBNull(userDataRow["UserSecurityQuestion"]))
377+ // UserData.SecurityQuestion = Convert.ToString(userDataRow["UserSecurityQuestion"]);
378+ //if (!Convert.IsDBNull(userDataRow["UserSecurityAnswer"]))
379+ // UserData.SecurityAnswer = Convert.ToString(userDataRow["UserSecurityAnswer"]);
380+ }
381+ else
382+ {
383+ // Load implicitly assigned groups - this happens via user's NT/AD groups that get loaded into the
384+ // user data group collection. When database group definitions are defined with the same name as
385+ // their NT/AD equivalents, this will allow automatic external group management.
386+ base.RefreshData(UserData.Groups, ProviderID);
387+ }
309388
310− UserData.Roles.Clear();
389+ // Administrator can lock out NT/AD user as well as database-only user via database
390+ if (!UserData.IsLockedOut && (object)userAccount != null && !Convert.IsDBNull(userAccount["LockedOut"]))
391+ UserData.IsLockedOut = Convert.ToBoolean(userAccount["LockedOut"]);
311392
312− // Load explicitly assigned roles
313− query = database.ParameterizedQueryString("SELECT ApplicationRoleID, ApplicationRoleName, ApplicationRoleDescription FROM AppRoleUserAccountDetail WHERE UserName = {0}", "name");
393+ // At this point an NT/AD based user will have a list of groups that is known to be available to the user. A database
394+ // user will have no groups defined yet. The next step will be to load any explicitly assigned groups the user is a
395+ // member of. Users can be explicitly assigned to any group, database or NT/AD group. This allows flexibility for
396+ // local database users to be assigned to an NT/AD group (in the database) as well as NT/AD users to be a member of
397+ // local database groups - allowing customizable role assignments to be fully managed via groups.
314398
315− using (IDataReader reader = dbConnection.ExecuteReader(query, UserData.Username))
316− {
317− userRoleDataTable.Load(reader);
318− }
399+ if (userAccountID != Guid.Empty)
400+ {
401+ // Filter explicitly assigned security groups for current user
402+ DataRow[] userGroups = securityContext.Tables[SecurityGroupUserAccountTable].Select(string.Format("UserAccountID = '{0}'", userAccountID));
319403
320− if (userRoleDataTable.Rows.Count == 0)
404+ foreach (DataRow row in userGroups)
321405 {
322− // Load implicitly assigned roles
323− query = database.ParameterizedQueryString("SELECT ApplicationRoleID, ApplicationRoleName, ApplicationRoleDescription FROM AppRoleSecurityGroupDetail WHERE SecurityGroupName = {0}", "groupName");
406+ // Locate associated security group record
407+ DataRow[] securityGroups = securityContext.Tables[SecurityGroupTable].Select(string.Format("ID = '{0}'", row["SecurityGroupID"]));
324408
325− foreach (string group in UserData.Groups)
409+ if (securityGroups.Length > 0)
326410 {
327− using (IDataReader reader = dbConnection.ExecuteReader(query, group))
411+ DataRow securityGroup = securityGroups[0];
412+
413+ if (!Convert.IsDBNull(securityGroup["Name"]))
328414 {
329− userRoleDataTable.Load(reader);
415+ // Just in case a database user was manually assigned to an NT/AD group, make sure to convert
416+ // the group name back to it's human readable name (from a SID). The UserInfo.SIDToAccountName
417+ // function will return the original parameter value if name cannot be converted - this allows
418+ // the function to work for database group names as well.
419+ string groupName = UserInfo.SIDToAccountName(Convert.ToString(securityGroup["Name"]));
420+
421+ if (!UserData.Groups.Contains(groupName, StringComparer.OrdinalIgnoreCase))
422+ UserData.Groups.Add(groupName);
330423 }
331424 }
425+ }
426+ }
427+
428+ // Explicitly assigned user roles will take precedence over any roles that may be implicitly available to the
429+ // user based on what group they may be a member of. In practice this means even though a user may be a member
430+ // of a group with role assignments, if there are any roles directly assigned to the user these will be their
431+ // effective roles. For example, if a user is in a group that is in the "Administrator" role and the user has
432+ // an explicit role assignment for the "Viewer" role - the user will be in the "Viewer" role only. This allows
433+ // individual users to have overridden role assignments even though they may also be part of a group.
434+ UserData.Roles.Clear();
435+
436+ // Filter explicitly assigned application roles for current user - this will return an empty set if no
437+ // explicitly defined roles exist for the user -or- user doesn't exist in the database.
438+ DataRow[] userApplicationRoles = securityContext.Tables[ApplicationRoleUserAccountTable].Select(string.Format("UserAccountID = '{0}'", userAccountID));
439+
440+ // If no explicitly assigned application roles are found for the current user, we check for implicitly assigned
441+ // application roles based on the role assignments of the groups the user is a member of.
442+ if (userApplicationRoles.Length == 0)
443+ {
444+ List<DataRow> implicitRoles = new List<DataRow>();
445+
446+ // Filter implicitly assigned application roles for each of the user's database and NT/AD groups. Note that
447+ // even if user is not defined in the database, an NT/AD group they are a member of may be associated with
448+ // a role - this allows the user to get a role assignment based on this group.
449+ foreach (string groupName in UserData.Groups)
450+ {
451+ // Convert NT/AD group names back to SIDs for lookup in the database
452+ string groupSID = UserInfo.AccountNameToSID(groupName);
332453
333− query = database.ParameterizedQueryString("SELECT AppRoleSecurityGroupDetail.ApplicationRoleID AS ApplicationRoleID, AppRoleSecurityGroupDetail.ApplicationRoleName AS ApplicationRoleName, AppRoleSecurityGroupDetail.ApplicationRoleDescription AS ApplicationRoleDescription FROM AppRoleSecurityGroupDetail, SecurityGroupUserAccountDetail WHERE AppRoleSecurityGroupDetail.SecurityGroupID = SecurityGroupUserAccountDetail.SecurityGroupID AND SecurityGroupUserAccountDetail.UserName = {0}", "name");
454+ // Locate associated security group record
455+ DataRow[] securityGroups = securityContext.Tables[SecurityGroupTable].Select(string.Format("Name = '{0}'", groupSID));
334456
335− using (IDataReader reader = dbConnection.ExecuteReader(query, UserData.Username))
457+ if (securityGroups.Length > 0)
336458 {
337− userRoleDataTable.Load(reader);
459+ // Found security group by name, access group ID to lookup application roles defined for the group
460+ DataRow securityGroup = securityGroups[0];
461+
462+ if (!Convert.IsDBNull(securityGroup["ID"]))
463+ implicitRoles.AddRange(securityContext.Tables[ApplicationRoleSecurityGroupTable].Select(string.Format("SecurityGroupID = '{0}'", securityGroup["ID"])));
338464 }
339465 }
340466
341− foreach (DataRow role in userRoleDataTable.Rows)
467+ userApplicationRoles = implicitRoles.ToArray();
468+ }
469+
470+ // Populate user roles collection - both ApplicationRoleUserAccount and ApplicationRoleSecurityGroup tables contain ApplicationRoleID column
471+ foreach (DataRow role in userApplicationRoles)
472+ {
473+ if (!Convert.IsDBNull(role["ApplicationRoleID"]))
342474 {
343− if (!Convert.IsDBNull(role["ApplicationRoleName"]))
475+ // Locate associated application role record
476+ DataRow[] applicationRoles = securityContext.Tables[ApplicationRoleTable].Select(string.Format("ID = '{0}'", role["ApplicationRoleID"]));
477+
478+ if (applicationRoles.Length > 0)
344479 {
345− roleName = Convert.ToString(role["ApplicationRoleName"]);
480+ // Found application role by ID, add role name to user roles if not already defined
481+ DataRow applicationRole = applicationRoles[0];
482+
483+ if (!Convert.IsDBNull(applicationRole["Name"]))
484+ {
485+ string roleName = Convert.ToString(applicationRole["Name"]);
346486
347− if (!UserData.Roles.Contains(roleName, StringComparer.InvariantCultureIgnoreCase))
348− UserData.Roles.Add(roleName);
487+ if (!UserData.Roles.Contains(roleName, StringComparer.OrdinalIgnoreCase))
488+ UserData.Roles.Add(roleName);
489+ }
349490 }
350491 }
492+ }
351493
352− // Cache last user roles
353− ThreadPool.QueueUserWorkItem(CacheLastUserRole);
494+ // Cache last user roles
495+ ThreadPool.QueueUserWorkItem(CacheLastUserRoles);
354496
355− return true;
356− }
497+ return true;
357498 }
358499 catch (Exception ex)
359500 {
@@ -410,7 +551,7 @@ namespace GSF.Security · 50 unmodified lines
410551 {
411552 Password = password;
412553
413− // Authenticate against backend datastore
554+ // Authenticate against backend data store
414555 UserData.IsAuthenticated =
415556 UserData.Password == password ||
416557 UserData.Password == SecurityProviderUtility.EncryptPassword(password) ||
@@ -442,7 +583,7 @@ namespace GSF.Security · 25 unmodified lines
442583 }
443584 }
444585
445− // If an exception occured during authentication, rethrow it after loging authentication attempt
586+ // If an exception occurred during authentication, rethrow it after logging authentication attempt
446587 if ((object)authenticationException != null)
447588 {
448589 m_lastException = authenticationException;
@@ -454,7 +595,7 @@ namespace GSF.Security · 5 unmodified lines
454595 }
455596
456597 /// <summary>
457− /// Changes user password in the backend datastore.
598+ /// Changes user password in the backend data store.
458599 /// </summary>
459600 /// <param name="oldPassword">User's current password.</param>
460601 /// <param name="newPassword">User's new password.</param>
@@ -472,22 +613,23 @@ namespace GSF.Security · 11 unmodified lines
472613 if (oldPassword == newPassword)
473614 throw new Exception("New password cannot be same as old password.");
474615
475− // Perform password change for internal users.
616+ // If needed, perform password change for internal NT/AD users.
476617 if (!UserData.IsExternal)
477618 return base.ChangePassword(oldPassword, newPassword);
478619
479620 // Verify old password.
480621 UserData.PasswordChangeDateTime = DateTime.MinValue;
622+
481623 if (!Authenticate(oldPassword))
482624 return false;
483625
484626 // Verify new password.
485− if (!Regex.IsMatch(newPassword, PasswordRequirementRegex))
486− throw new SecurityException(PasswordRequirementError);
627+ if (!Regex.IsMatch(newPassword, m_passwordRequirementsRegex))
628+ throw new SecurityException(m_passwordRequirementsError);
487629
488630 using (IDbConnection dbConnection = (new AdoDataConnection(SettingsCategory)).Connection)
489631 {
490− if (dbConnection == null)
632+ if ((object)dbConnection == null)
491633 return false;
492634
493635 bool oracle = dbConnection.GetType().Name == "OracleConnection";
@@ -550,7 +692,7 @@ namespace GSF.Security · 56 unmodified lines
550692 /// <returns>true if logging was successful, otherwise false.</returns>
551693 protected virtual bool LogAuthenticationAttempt(bool loginSuccess)
552694 {
553− if (UserData != null && !string.IsNullOrWhiteSpace(UserData.Username))
695+ if ((object)UserData != null && !string.IsNullOrWhiteSpace(UserData.Username))
554696 {
555697 string message = string.Format("User \"{0}\" login attempt {1}.", UserData.Username, loginSuccess ? "succeeded using " + (m_successfulPassThroughAuthentication ? "pass-through authentication" : "user acquired password") : "failed");
556698 EventLogEntryType entryType = loginSuccess ? EventLogEntryType.SuccessAudit : EventLogEntryType.FailureAudit;
@@ -589,7 +731,7 @@ namespace GSF.Security · 32 unmodified lines
589731 }
590732
591733 /// <summary>
592− /// Logs information about an encountered exception to the backend datastore.
734+ /// Logs information about an encountered exception to the backend data store.
593735 /// </summary>
594736 /// <param name="source">Source of the exception.</param>
595737 /// <param name="message">Detailed description of the exception.</param>
@@ -631,50 +773,43 @@ namespace GSF.Security · 35 unmodified lines
631773 return configSettings["LdapPath"].Value;
632774 }
633775
634− // Cache last user role and monitor for changes
635− private void CacheLastUserRole(object state)
776+ // Cache last user roles and monitor for changes
777+ private void CacheLastUserRoles(object state)
636778 {
637779 try
638780 {
639− // Using an interprocess cache for user roles
781+ // Using an inter-process cache for user roles
640782 UserRoleCache userRoleCache = UserRoleCache.GetCurrentCache();
641− string cachedRole, currentRole;
783+ HashSet<string> currentRoles;
784+ string[] cachedRoles;
642785
643− // Attempt to retrieve cached user role
644− cachedRole = userRoleCache[UserData.Username];
786+ // Retrieve current user roles
787+ currentRoles = new HashSet<string>(UserData.Roles, StringComparer.OrdinalIgnoreCase);
645788
646− // Determine current role with maximum rights for user
647− string[] roles = UserData.Roles.ToArray();
648−
649− // Technically a user can be granted multiple roles, pick the highest ordered role
650− if (roles.Contains("Administrator", StringComparer.InvariantCultureIgnoreCase))
651− currentRole = "Administrator";
652− else if (roles.Contains("Editor", StringComparer.InvariantCultureIgnoreCase))
653− currentRole = "Editor";
654− else
655− currentRole = "Viewer";
789+ // Attempt to retrieve cached user roles
790+ cachedRoles = userRoleCache[UserData.Username];
656791
657− bool roleChanged = false;
792+ bool rolesChanged = false;
658793 string message;
659794 EventLogEntryType entryType;
660795
661− if (string.IsNullOrEmpty(cachedRole))
796+ if ((object)cachedRoles == null || cachedRoles.Length == 0)
662797 {
663798 // New user access granted
664− message = string.Format("New user \"{0}\" granted access with role \"{1}\".", UserData.Username, currentRole);
799+ message = string.Format("New user \"{0}\" granted access with role{1} \"{2}\".", UserData.Username, currentRoles.Count == 1 ? "" : "s", currentRoles.ToDelimitedString(", "));
665800 entryType = EventLogEntryType.Information;
666− roleChanged = true;
801+ rolesChanged = true;
667802 }
668− else if (string.Compare(cachedRole, currentRole, true) != 0)
803+ else if (!currentRoles.SetEquals(cachedRoles))
669804 {
670805 // User role access changed
671− message = string.Format("Existing user \"{0}\" granted access with new role \"{1}\" - role assignment is different from last login, was \"{2}\".", UserData.Username, currentRole, cachedRole);
806+ message = string.Format("Existing user \"{0}\" granted access with new role{1} \"{2}\" - role assignment is different from last login, was \"{3}\".", UserData.Username, currentRoles.Count == 1 ? "" : "s", currentRoles.ToDelimitedString(", "), cachedRoles.ToDelimitedString(", "));
672807 entryType = EventLogEntryType.Warning;
673− roleChanged = true;
808+ rolesChanged = true;
674809 }
675810 else
676811 {
677− message = string.Format("Existing user \"{0}\" granted access with role \"{1}\" - role assignment is the same as last login.", UserData.Username, currentRole);
812+ message = string.Format("Existing user \"{0}\" granted access with role{1} \"{2}\" - role assignment is the same as last login.", UserData.Username, currentRoles.Count == 1 ? "" : "s", currentRoles.ToDelimitedString(", "));
678813 entryType = EventLogEntryType.SuccessAudit;
679814 }
680815
@@ -689,8 +824,8 @@ namespace GSF.Security · 8 unmodified lines
689824 }
690825
691826 // If role has changed, update cache
692− if (roleChanged)
693− userRoleCache[UserData.Username] = currentRole;
827+ if (rolesChanged)
828+ userRoleCache[UserData.Username] = currentRoles.ToArray();
694829 }
695830 catch (Exception ex)
696831 {
@@ -706,5 +841,75 @@ namespace GSF.Security · 9 unmodified lines
706841 }
707842
708843 #endregion
844+
845+ #region [ Static ]
846+
847+ // Static Fields
848+ static private readonly string[] s_securityTables =
849+ {
850+ UserAccountTable, // User accounts
851+ SecurityGroupTable, // Security groups
852+ SecurityGroupUserAccountTable, // Security group user accounts (i.e., users in a group)
853+ ApplicationRoleTable, // Application roles (node specific)
854+ ApplicationRoleUserAccountTable, // Application role assignments for user accounts
855+ ApplicationRoleSecurityGroupTable // Application role assignments for security groups
856+ };
857+
858+ // Static Methods
859+
860+ /// <summary>
861+ /// Extracts the current security context from the database and kicks off a background caching operation.
862+ /// </summary>
863+ /// <param name="connection">Existing database connection used to extract security context.</param>
864+ public static void PrepareSecurityContext(IDbConnection connection)
865+ {
866+ DataSet securityContext = ExtractSecurityContext(connection);
867+ Thread cacheSecurityContext = new Thread(() => AdoSecurityCache.GetCurrentCache().DataSet = securityContext);
868+ cacheSecurityContext.IsBackground = true;
869+ cacheSecurityContext.Start();
870+ }
871+
872+ /// <summary>
873+ /// Extracts the current security context from the database.
874+ /// </summary>
875+ /// <param name="connection">Existing database connection used to extract security context.</param>
876+ /// <returns>A new <see cref="DataSet"/> containing the latest security context.</returns>
877+ public static DataSet ExtractSecurityContext(IDbConnection connection)
878+ {
879+ DataSet securityContext = new DataSet("AdoSecurityContext");
880+
881+ // Access configuration file system settings
882+ CategorizedSettingsElementCollection systemSettings = ConfigurationFile.Current.Settings["systemSettings"];
883+
884+ // Make sure NodeID setting exists
885+ systemSettings.Add("NodeID", Guid.NewGuid().ToString(), "Unique Node ID");
886+
887+ // Get NodeID as currently defined in configuration file
888+ Guid nodeID = systemSettings["NodeID"].ValueAs<Guid>();
889+
890+ foreach (string securityTable in s_securityTables)
891+ {
892+ AddSecurityContextTable(connection, securityContext, securityTable, securityTable == ApplicationRoleTable ? nodeID : default(Guid));
893+ }
894+
895+ return securityContext;
896+ }
897+
898+ private static void AddSecurityContextTable(IDbConnection connection, DataSet securityContext, string tableName, Guid nodeID)
899+ {
900+ string tableQuery;
901+
902+ if (nodeID == default(Guid))
903+ tableQuery = string.Format("SELECT * FROM {0}", tableName);
904+ else
905+ tableQuery = string.Format("SELECT * FROM {0} WHERE NodeID = '{1}'", tableName, nodeID);
906+
907+ using (IDataReader reader = connection.ExecuteReader(tableQuery))
908+ {
909+ securityContext.Tables.Add(tableName).Load(reader);
910+ }
911+ }
912+
913+ #endregion
709914 }
710915 }
Source/Libraries/GSF.Security/GSF.Security.csproj+1−0
@@ -143,6 +143,7 @@ · 142 unmodified lines
143143 </ItemGroup>
144144 <ItemGroup>
145145 <Compile Include="AdoSecurityProvider.cs" />
146+ <Compile Include="AdoSecurityCache.cs" />
146147 <Compile Include="UserRoleCache.cs" />
147148 <Compile Include="LdapSecurityProvider.cs" />
148149 <Compile Include="RestrictAccessAttribute.cs" />
Source/Libraries/GSF.Security/LdapSecurityProvider.cs+29−28
@@ -285,7 +285,7 @@ namespace GSF.Security · 284 unmodified lines
285285 // Load settings from the specified category.
286286 ConfigurationFile config = ConfigurationFile.Current;
287287 CategorizedSettingsElementCollection settings = config.Settings[SettingsCategory];
288− settings.Add("EnableOfflineCaching", m_enableOfflineCaching, "True to enable caching of user information for authentication in offline state, otherwise False.");
288+ settings.Add("EnableOfflineCaching", m_enableOfflineCaching, "True to enable caching of user group information for authentication in offline state, otherwise False.");
289289 settings.Add("CacheRetryDelayInterval", m_cacheRetryDelayInterval, "Wait interval, in milliseconds, before retrying load of user data cache.");
290290 settings.Add("CacheMaximumRetryAttempts", m_cacheMaximumRetryAttempts, "Maximum retry attempts allowed for loading user data cache.");
291291 EnableOfflineCaching = settings["EnableOfflineCaching"].ValueAs(m_enableOfflineCaching);
@@ -312,15 +312,19 @@ namespace GSF.Security · 20 unmodified lines
312312 {
313313 // Validate with current thread principal.
314314 m_windowsPrincipal = Thread.CurrentPrincipal as WindowsPrincipal;
315− UserData.IsAuthenticated = m_windowsPrincipal != null && !string.IsNullOrEmpty(UserData.LoginID) &&
316− string.Compare(m_windowsPrincipal.Identity.Name, UserData.LoginID, true) == 0 && m_windowsPrincipal.Identity.IsAuthenticated;
315+
316+ UserData.IsAuthenticated =
317+ (object)m_windowsPrincipal != null &&
318+ !string.IsNullOrEmpty(UserData.LoginID) &&
319+ string.Compare(m_windowsPrincipal.Identity.Name, UserData.LoginID, StringComparison.OrdinalIgnoreCase) == 0 &&
320+ m_windowsPrincipal.Identity.IsAuthenticated;
317321 }
318322 else
319323 {
320324 // Validate by performing network logon.
321325 string[] userParts = UserData.LoginID.Split('\\');
322326 m_windowsPrincipal = UserInfo.AuthenticateUser(userParts[0], userParts[1], password) as WindowsPrincipal;
323− UserData.IsAuthenticated = m_windowsPrincipal != null && m_windowsPrincipal.Identity.IsAuthenticated;
327+ UserData.IsAuthenticated = (object)m_windowsPrincipal != null && m_windowsPrincipal.Identity.IsAuthenticated;
324328 }
325329
326330 return UserData.IsAuthenticated;
@@ -332,10 +336,10 @@ namespace GSF.Security · 5 unmodified lines
332336 /// <returns>true if <see cref="SecurityProviderBase.UserData"/> is refreshed, otherwise false.</returns>
333337 public override bool RefreshData()
334338 {
335− bool result;
336−
337339 // For consistency with WindowIdentity principal, user groups are loaded into Roles collection
338− if (result = RefreshData(UserData.Roles, ProviderID))
340+ bool result = RefreshData(UserData.Roles, ProviderID);
341+
342+ if (result)
339343 {
340344 string[] parts;
341345
@@ -360,7 +364,7 @@ namespace GSF.Security · 18 unmodified lines
360364 /// <returns>true if <see cref="SecurityProviderBase.UserData"/> is refreshed, otherwise false.</returns>
361365 protected virtual bool RefreshData(List<string> groupCollection, int providerID)
362366 {
363− if (groupCollection == null)
367+ if ((object)groupCollection == null)
364368 throw new ArgumentNullException("groupCollection");
365369
366370 if (string.IsNullOrEmpty(UserData.Username))
@@ -393,6 +397,7 @@ namespace GSF.Security · 26 unmodified lines
393397
394398 // Create user info object using specified LDAP path if provided
395399 string ldapPath = GetLdapPath();
400+
396401 if (string.IsNullOrEmpty(ldapPath))
397402 user = new UserInfo(UserData.Username);
398403 else
@@ -428,7 +433,7 @@ namespace GSF.Security · 29 unmodified lines
428433 groupCollection.Add(groupName);
429434 }
430435
431− if (userDataCache != null)
436+ if ((object)userDataCache != null)
432437 {
433438 // Cache user data so that information can be loaded later if domain is unavailable
434439 userDataCache[UserData.LoginID] = UserData;
@@ -440,8 +445,9 @@ namespace GSF.Security · 5 unmodified lines
440445 else
441446 {
442447 // Attempt to load previously cached user information when domain is offline
443− UserData cachedUserData = null;
444− if (userDataCache != null && userDataCache.TryGetUserData(UserData.LoginID, out cachedUserData))
448+ UserData cachedUserData;
449+
450+ if ((object)userDataCache != null && userDataCache.TryGetUserData(UserData.LoginID, out cachedUserData))
445451 {
446452 // Copy relevant cached user information
447453 UserData.FirstName = cachedUserData.FirstName;
@@ -473,19 +479,19 @@ namespace GSF.Security · 25 unmodified lines
473479 }
474480 finally
475481 {
476− if (user != null)
482+ if ((object)user != null)
477483 {
478484 user.PersistSettings = false;
479485 user.Dispose();
480486 }
481487
482− if (userDataCache != null)
488+ if ((object)userDataCache != null)
483489 userDataCache.Dispose();
484490 }
485491 }
486492
487493 /// <summary>
488− /// Changes user password in the backend datastore.
494+ /// Changes user password in the backend data store.
489495 /// </summary>
490496 /// <param name="oldPassword">User's current password.</param>
491497 /// <param name="newPassword">User's new password.</param>
@@ -534,17 +540,17 @@ namespace GSF.Security · 42 unmodified lines
534540 catch (TargetInvocationException ex)
535541 {
536542 // Propagate password change error
537− if (ex.InnerException == null)
543+ if ((object)ex.InnerException == null)
538544 throw new SecurityException(ex.Message, ex);
539545 else
540546 throw new SecurityException(ex.InnerException.Message, ex);
541547 }
542548 finally
543549 {
544− if (user != null)
550+ if ((object)user != null)
545551 user.Dispose();
546552
547− if (context != null)
553+ if ((object)context != null)
548554 UserInfo.EndImpersonation(context);
549555
550556 RefreshData();
@@ -572,19 +578,14 @@ namespace GSF.Security · 21 unmodified lines
572578 /// <returns>The LDAP path.</returns>
573579 protected virtual string GetLdapPath()
574580 {
575− if (ConnectionString.StartsWith("LDAP://", StringComparison.InvariantCultureIgnoreCase) ||
576− ConnectionString.StartsWith("LDAPS://", StringComparison.InvariantCultureIgnoreCase))
577− {
581+ if (ConnectionString.StartsWith("LDAP://", StringComparison.InvariantCultureIgnoreCase) || ConnectionString.StartsWith("LDAPS://", StringComparison.InvariantCultureIgnoreCase))
578582 return ConnectionString;
579− }
580− else
583+
584+ foreach (KeyValuePair<string, string> pair in ConnectionString.ParseKeyValuePairs())
581585 {
582− foreach (KeyValuePair<string, string> pair in ConnectionString.ParseKeyValuePairs())
583− {
584− if (pair.Value.StartsWith("LDAP://", StringComparison.InvariantCultureIgnoreCase) ||
585− pair.Value.StartsWith("LDAPS://", StringComparison.InvariantCultureIgnoreCase))
586− return pair.Value;
587− }
586+ if (pair.Value.StartsWith("LDAP://", StringComparison.InvariantCultureIgnoreCase) ||
587+ pair.Value.StartsWith("LDAPS://", StringComparison.InvariantCultureIgnoreCase))
588+ return pair.Value;
588589 }
589590
590591 return null;
Source/Libraries/GSF.Security/SecurityProviderBase.cs+3−3
@@ -284,7 +284,7 @@ namespace GSF.Security · 283 unmodified lines
284284 }
285285
286286 /// <summary>
287− /// Gets or sets <see cref="SecurePassword"/> as cleartext password.
287+ /// Gets or sets <see cref="SecurePassword"/> as clear text password.
288288 /// </summary>
289289 public unsafe string Password
290290 {
@@ -314,7 +314,7 @@ namespace GSF.Security · 23 unmodified lines
314314 {
315315 SecureString securePassword = new SecureString();
316316
317− if (value == null)
317+ if ((object)value == null)
318318 value = string.Empty;
319319
320320 foreach (char c in value)
@@ -621,7 +621,7 @@ namespace GSF.Security · 300 unmodified lines
621621 {
622622 m_disposed = true; // Prevent duplicate dispose.
623623
624− if (Disposed != null)
624+ if ((object)Disposed != null)
625625 Disposed(this, EventArgs.Empty);
626626 }
627627 }
Source/Libraries/GSF.Security/UserData.cs+99−23
@@ -25,7 +25,7 @@ · 24 unmodified lines
2525 // 01/14/2011 - Pinal C. Patel
2626 // Updated field attributes used by XmlSerializer.
2727 // 12/20/2012 - Starlynn Danyelle Gilliam
28−// Modifeid Header.
28+// Modified Header.
2929 //
3030 //******************************************************************************************************
3131
@@ -37,7 +37,7 @@ using System.Xml.Serialization; · 5 unmodified lines
3737 namespace GSF.Security
3838 {
3939 /// <summary>
40− /// A serializable class that contains information about a user defined in the security datastore.
40+ /// A serializable class that contains information about a user defined in the security data store.
4141 /// </summary>
4242 [XmlType(Namespace = ""), DataContract(Namespace = ""), Serializable]
4343 public class UserData
@@ -73,115 +73,191 @@ namespace GSF.Security · 29 unmodified lines
7373 /// <summary>
7474 /// Gets the user's login ID.
7575 /// </summary>
76− public string LoginID { get; set; }
76+ public string LoginID
77+ {
78+ get;
79+ set;
80+ }
7781
7882 /// <summary>
7983 /// Gets the user's login name.
8084 /// </summary>
8185 [XmlElement, DataMember(Order = 0)]
82− public string Username { get; set; }
86+ public string Username
87+ {
88+ get;
89+ set;
90+ }
8391
8492 /// <summary>
8593 /// Gets the user's password.
8694 /// </summary>
8795 [XmlElement, DataMember(Order = 1)]
88− public string Password { get; set; }
96+ public string Password
97+ {
98+ get;
99+ set;
100+ }
89101
90102 /// <summary>
91103 /// Gets the user's first name.
92104 /// </summary>
93105 [XmlElement, DataMember(Order = 2)]
94− public string FirstName { get; set; }
106+ public string FirstName
107+ {
108+ get;
109+ set;
110+ }
95111
96112 /// <summary>
97113 /// Gets the user's last name.
98114 /// </summary>
99115 [XmlElement, DataMember(Order = 3)]
100− public string LastName { get; set; }
116+ public string LastName
117+ {
118+ get;
119+ set;
120+ }
101121
102122 /// <summary>
103123 /// Gets the user's company name.
104124 /// </summary>
105125 [XmlElement, DataMember(Order = 4)]
106− public string CompanyName { get; set; }
126+ public string CompanyName
127+ {
128+ get;
129+ set;
130+ }
107131
108132 /// <summary>
109133 /// Gets the user's phone number.
110134 /// </summary>
111135 [XmlElement, DataMember(Order = 5)]
112− public string PhoneNumber { get; set; }
136+ public string PhoneNumber
137+ {
138+ get;
139+ set;
140+ }
113141
114142 /// <summary>
115143 /// Gets the user's email address.
116144 /// </summary>
117145 [XmlElement, DataMember(Order = 6)]
118− public string EmailAddress { get; set; }
146+ public string EmailAddress
147+ {
148+ get;
149+ set;
150+ }
119151
120152 /// <summary>
121153 /// Gets the user's security question.
122154 /// </summary>
123155 [XmlElement, DataMember(Order = 7)]
124− public string SecurityQuestion { get; set; }
156+ public string SecurityQuestion
157+ {
158+ get;
159+ set;
160+ }
125161
126162 /// <summary>
127163 /// Gets the user's security answer.
128164 /// </summary>
129165 [XmlElement, DataMember(Order = 8)]
130− public string SecurityAnswer { get; set; }
166+ public string SecurityAnswer
167+ {
168+ get;
169+ set;
170+ }
131171
132172 /// <summary>
133173 /// Gets the UTC date and time when user must change the password.
134174 /// </summary>
135175 [XmlElement, DataMember(Order = 9)]
136− public DateTime PasswordChangeDateTime { get; set; }
176+ public DateTime PasswordChangeDateTime
177+ {
178+ get;
179+ set;
180+ }
137181
138182 /// <summary>
139183 /// Gets the UTC date and time when user account was created.
140184 /// </summary>
141185 [XmlElement, DataMember(Order = 10)]
142− public DateTime AccountCreatedDateTime { get; set; }
186+ public DateTime AccountCreatedDateTime
187+ {
188+ get;
189+ set;
190+ }
143191
144192 /// <summary>
145− /// Gets a boolean value that indicates whether the user is defined in the backend security datastore.
193+ /// Gets a boolean value that indicates whether the user is defined in the backend security data store.
146194 /// </summary>
147195 [XmlElement, DataMember(Order = 11)]
148− public bool IsDefined { get; set; }
196+ public bool IsDefined
197+ {
198+ get;
199+ set;
200+ }
149201
150202 /// <summary>
151− /// Gets a boolean value that indicates whether the user is defined as an external user in the backend security datastore.
203+ /// Gets a boolean value that indicates whether the user is defined as an external user in the backend security data store.
152204 /// </summary>
153205 [XmlElement, DataMember(Order = 12)]
154− public bool IsExternal { get; set; }
206+ public bool IsExternal
207+ {
208+ get;
209+ set;
210+ }
155211
156212 /// <summary>
157213 /// Gets a boolean value that indicates whether the user account has been disabled.
158214 /// </summary>
159215 [XmlElement, DataMember(Order = 13)]
160− public bool IsDisabled { get; set; }
216+ public bool IsDisabled
217+ {
218+ get;
219+ set;
220+ }
161221
162222 /// <summary>
163223 /// Gets a boolean value that indicates whether the user account has been locked due to numerous unsuccessful login attempts.
164224 /// </summary>
165225 [XmlElement, DataMember(Order = 14)]
166− public bool IsLockedOut { get; set; }
226+ public bool IsLockedOut
227+ {
228+ get;
229+ set;
230+ }
167231
168232 /// <summary>
169233 /// Gets a boolean value indicating whether or not the user has been authenticated.
170234 /// </summary>
171235 [XmlElement, DataMember(Order = 15)]
172− public bool IsAuthenticated { get; set; }
236+ public bool IsAuthenticated
237+ {
238+ get;
239+ set;
240+ }
173241
174242 /// <summary>
175243 /// Gets a read-only list of all the groups the user belongs to.
176244 /// </summary>
177245 [XmlArray, XmlArrayItem("Group"), DataMember(Order = 16)]
178− public List<string> Groups { get; set; }
246+ public List<string> Groups
247+ {
248+ get;
249+ set;
250+ }
179251
180252 /// <summary>
181253 /// Gets a read-only list of all the roles assigned to the user.
182254 /// </summary>
183255 [XmlArray, XmlArrayItem("Role"), DataMember(Order = 17)]
184− public List<string> Roles { get; set; }
256+ public List<string> Roles
257+ {
258+ get;
259+ set;
260+ }
185261
186262 #endregion
187263
Source/Libraries/GSF.Security/UserDataCache.cs+17−7
@@ -24,7 +24,7 @@ · 23 unmodified lines
2424 // Renamed RetryDelayInterval and MaximumRetryAttempts settings persisted to the config file
2525 // to CacheRetryDelayInterval and CacheMaximumRetryAttempts for clarity.
2626 // 08/12/2011 - J. Ritchie Carroll
27−// Modifed static GetCurrentCache to accept settings category of host security provider
27+// Modified static GetCurrentCache to accept settings category of host security provider
2828 // implementation in case the category has been changed from the default value by the consumer.
2929 // 08/16/2011 - Pinal C. Patel
3030 // Modified GetCurrentCache() to just set the FileName property and not the RetryDelayInterval,
@@ -46,8 +46,19 @@ using GSF.Threading; · 15 unmodified lines
4646 namespace GSF.Security
4747 {
4848 /// <summary>
49− /// Represents a secured interprocess cache for a <see cref="Dictionary{TKey,TValue}"/> of serialized <see cref="UserData"/>.
49+ /// Represents a secured inter-process cache for a <see cref="Dictionary{TKey,TValue}"/> of serialized <see cref="UserData"/>.
5050 /// </summary>
51+ /// <remarks>
52+ /// This is a personal user data cache that only contains basic LDAP information for the user. It is used to load the local and
53+ /// Active Directory groups a user is associated with when the user no longer has access to its domain server. This can happen
54+ /// when a laptop that is normally connected to the Active Directory domain gets shutdown then restarted without access to the
55+ /// domain, for example, on an airplane - in this mode the user can successfully still login to the laptop to their using domain
56+ /// account cached by Windows but the groups the user is in will no longer be accessible. If role based security happens to be
57+ /// based on Active Directory groups, this cache will make sure the user can still have needed role based access even when the
58+ /// domain is unavailable. This cache is maintained as a separate user cache from the system level <see cref="AdoSecurityCache"/>
59+ /// since the user data cache only contains group information and is used by the <see cref="LdapSecurityProvider"/> which can be
60+ /// used independently of the <see cref="AdoSecurityProvider"/>.
61+ /// </remarks>
5162 public class UserDataCache : InterprocessCache
5263 {
5364 #region [ Members ]
@@ -59,7 +70,7 @@ namespace GSF.Security · 5 unmodified lines
5970
6071 // Fields
6172 private Dictionary<string, UserData> m_userDataTable; // Internal dictionary of serialized user data
62− private readonly object m_userDataTableLock; // Lock object
73+ private readonly object m_userDataTableLock; // Lock object for internal dictionary
6374 private int m_providerID; // Unique provider ID used to distinguish cached user data that may be different based on provider
6475
6576 #endregion
@@ -108,7 +119,7 @@ namespace GSF.Security · 42 unmodified lines
108119 }
109120
110121 /// <summary>
111− /// Gets ot sets unique provider ID used to distinguish cached user data that may be different based on provider.
122+ /// Gets or sets unique provider ID used to distinguish cached user data that may be different based on provider.
112123 /// </summary>
113124 public int ProviderID
114125 {
@@ -183,7 +194,7 @@ namespace GSF.Security · 68 unmodified lines
183194 }
184195
185196 /// <summary>
186− /// Initiates interprocess synchronized save of user data cache.
197+ /// Initiates inter-process synchronized save of user data cache.
187198 /// </summary>
188199 public override void Save()
189200 {
@@ -195,7 +206,7 @@ namespace GSF.Security · 5 unmodified lines
195206 serializedUserDataTable = Serialization.Serialize(m_userDataTable, SerializationFormat.Binary);
196207 }
197208
198− // File data is the serialized user data table, assigmnent will initiate auto-save if needed
209+ // File data is the serialized user data table, assignment will initiate auto-save if needed
199210 FileData = serializedUserDataTable;
200211 }
201212
@@ -296,6 +307,5 @@ namespace GSF.Security · 94 unmodified lines
296307 }
297308
298309 #endregion
299−
300310 }
301311 }
Source/Libraries/GSF.Security/UserRoleCache.cs+51−44
@@ -21,20 +21,27 @@ · 20 unmodified lines
2121 //
2222 //******************************************************************************************************
2323
24−using GSF.Collections;
25−using GSF.IO;
26−using GSF.Security.Cryptography;
27−using GSF.Threading;
2824 using System;
2925 using System.Collections.Generic;
3026 using System.IO;
3127 using System.Security.Cryptography;
28+using GSF.Collections;
29+using GSF.IO;
30+using GSF.Security.Cryptography;
31+using GSF.Threading;
3232
3333 namespace GSF.Security
3434 {
3535 /// <summary>
36− /// Represents a secured interprocess cache for a <see cref="Dictionary{TKey,TValue}"/> of serialized user role information.
36+ /// Represents a secured inter-process cache for a <see cref="Dictionary{TKey,TValue}"/> of serialized user role information.
3737 /// </summary>
38+ /// <remarks>
39+ /// This is a system cache that contains the highest order role assignment for each user that has logged in successfully. This
40+ /// 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
41+ /// different than what is in the current cache. Any kind of role changes are logged as security events in the Windows event
42+ /// log for auditing. Note that this is kept as a separate cache from the <see cref="AdoSecurityCache"/> since the user role
43+ /// cache is used for auditing and contains information relative to a user's role at last login.
44+ /// </remarks>
3845 public class UserRoleCache : InterprocessCache
3946 {
4047 #region [ Members ]
@@ -45,8 +52,8 @@ namespace GSF.Security · 4 unmodified lines
4552 private const string DefaultCacheFileName = "UserRoleCache.bin";
4653
4754 // Fields
48− private Dictionary<string, string> m_userRoles; // Internal dictionary of serialized user roles
49− private readonly object m_userRolesLock; // Lock object
55+ private Dictionary<string, string[]> m_userRoles; // Internal dictionary of serialized user roles
56+ private readonly object m_userRolesLock; // Lock object for internal dictionary
5057
5158 #endregion
5259
@@ -57,7 +64,7 @@ namespace GSF.Security · 4 unmodified lines
5764 public UserRoleCache(int maximumConcurrentLocks = InterprocessReaderWriterLock.DefaultMaximumConcurrentLocks)
5865 : base(maximumConcurrentLocks)
5966 {
60− m_userRoles = new Dictionary<string, string>();
67+ m_userRoles = new Dictionary<string, string[]>();
6168 m_userRolesLock = new object();
6269 }
6370
@@ -66,11 +73,11 @@ namespace GSF.Security · 2 unmodified lines
6673 /// <summary>
6774 /// Gets a copy of the internal user role dictionary.
6875 /// </summary>
69− public Dictionary<string, string> UserRoles
76+ public Dictionary<string, string[]> UserRoles
7077 {
7178 get
7279 {
73− Dictionary<string, string> userRoles;
80+ Dictionary<string, string[]> userRoles;
7481
7582 // We wait until the user roles cache is loaded before attempting to access it
7683 WaitForDataReady();
@@ -78,8 +85,8 @@ namespace GSF.Security · 1 unmodified line
7885 // Wait for thread level lock on dictionary
7986 lock (m_userRolesLock)
8087 {
81− // Make a copy of the user role table for external use
82− userRoles = new Dictionary<string, string>(m_userRoles);
88+ // Make a copy of the user role dictionary for external use
89+ userRoles = new Dictionary<string, string[]>(m_userRoles);
8390 }
8491
8592 return userRoles;
@@ -87,17 +94,17 @@ namespace GSF.Security · 1 unmodified line
8794 }
8895
8996 /// <summary>
90− /// Gets or sets access role for given <paramref name="userName"/>.
97+ /// Gets or sets access roles for given <paramref name="userName"/>.
9198 /// </summary>
9299 /// <param name="userName">User name for associated access role to load or save.</param>
93− /// <returns>Access role for given <paramref name="userName"/> if found; otherwise <c>null</c>.</returns>
94− public string this[string userName]
100+ /// <returns>Access roles for given <paramref name="userName"/> if found; otherwise <c>null</c>.</returns>
101+ public string[] this[string userName]
95102 {
96103 get
97104 {
98− string role;
99− TryGetUserRole(userName, out role);
100− return role;
105+ string[] roles;
106+ TryGetUserRole(userName, out roles);
107+ return roles;
101108 }
102109 set
103110 {
@@ -113,9 +120,9 @@ namespace GSF.Security · 9 unmodified lines
113120 /// Attempts to retrieve access role for given <paramref name="userName"/>.
114121 /// </summary>
115122 /// <param name="userName">User name associated with access role to retrieve.</param>
116− /// <param name="role">Access role to populate if found.</param>
117− /// <returns><c>true</c> if access role for given <paramref name="userName"/> was retrieved; otherwise <c>false</c>.</returns>
118− public bool TryGetUserRole(string userName, out string role)
123+ /// <param name="roles">Access roles to populate if found.</param>
124+ /// <returns><c>true</c> if access roles for given <paramref name="userName"/> were retrieved; otherwise <c>false</c>.</returns>
125+ public bool TryGetUserRole(string userName, out string[] roles)
119126 {
120127 string hash = HashLoginID(userName);
121128 bool result;
@@ -123,42 +130,42 @@ namespace GSF.Security · 1 unmodified line
123130 // We wait until the cache is loaded before attempting to access it
124131 WaitForDataReady();
125132
126− // Wait for thread level lock on user info table
133+ // Wait for thread level lock on user info dictionary
127134 lock (m_userRolesLock)
128135 {
129136 // Attempt to lookup persisted access role based on hash of user name
130− result = m_userRoles.TryGetValue(hash, out role);
137+ result = m_userRoles.TryGetValue(hash, out roles);
131138 }
132139
133140 return result;
134141 }
135142
136143 /// <summary>
137− /// Serializes the <paramref name="role"/> for the given <paramref name="userName"/> into the <see cref="UserRoleCache"/>.
144+ /// Serializes the <paramref name="roles"/> for the given <paramref name="userName"/> into the <see cref="UserRoleCache"/>.
138145 /// </summary>
139146 /// <param name="userName">User name associated with access role to retrieve.</param>
140− /// <param name="role">Access role to update or populate.</param>
147+ /// <param name="roles">Access roles to update or populate.</param>
141148 /// <remarks>
142149 /// <para>
143− /// This will add an entry into the user info cache for <paramref name="userName"/> if it doesn't exist;
150+ /// This will add an entry into the user roles cache for <paramref name="userName"/> if it doesn't exist;
144151 /// otherwise existing entry will be updated.
145152 /// </para>
146153 /// <para>
147154 /// Updates are automatically queued up for serialization so user does not need to call <see cref="Save"/>.
148155 /// </para>
149156 /// </remarks>
150− public void SaveUserRole(string userName, string role)
157+ public void SaveUserRole(string userName, string[] roles)
151158 {
152159 string hash = HashLoginID(userName);
153160
154161 // We wait until the cache is loaded before attempting to access it
155162 WaitForDataReady();
156163
157− // Wait for thread level lock on user data table
164+ // Wait for thread level lock on user data dictionary
158165 lock (m_userRolesLock)
159166 {
160− // Assign new user information to user data table
161− m_userRoles[hash] = role;
167+ // Assign new user information to user data dictionary
168+ m_userRoles[hash] = roles;
162169 }
163170
164171 // Queue up a serialization for this new user information
@@ -172,7 +179,7 @@ namespace GSF.Security · 7 unmodified lines
172179 public void MergeLeft(UserRoleCache other)
173180 {
174181 // Merge other roles into local ones
175− Dictionary<string, string> mergedUserRoles = UserRoles.Merge(other.UserRoles);
182+ Dictionary<string, string[]> mergedUserRoles = UserRoles.Merge(other.UserRoles);
176183
177184 // Wait for thread level lock on dictionary
178185 lock (m_userRolesLock)
@@ -192,7 +199,7 @@ namespace GSF.Security · 13 unmodified lines
192199 public void MergeRight(UserRoleCache other)
193200 {
194201 // Merge other roles into local ones
195− Dictionary<string, string> mergedUserRoles = other.UserRoles.Merge(UserRoles);
202+ Dictionary<string, string[]> mergedUserRoles = other.UserRoles.Merge(UserRoles);
196203
197204 // Wait for thread level lock on dictionary
198205 lock (m_userRolesLock)
@@ -206,7 +213,7 @@ namespace GSF.Security · 7 unmodified lines
206213 }
207214
208215 /// <summary>
209− /// Initiates interprocess synchronized save of user role cache.
216+ /// Initiates inter-process synchronized save of user role cache.
210217 /// </summary>
211218 public override void Save()
212219 {
@@ -218,7 +225,7 @@ namespace GSF.Security · 5 unmodified lines
218225 serializedUserDataTable = Serialization.Serialize(m_userRoles, SerializationFormat.Binary);
219226 }
220227
221− // File data is the serialized user roles dictionary, assigmnent will initiate auto-save if needed
228+ // File data is the serialized user roles dictionary, assignment will initiate auto-save if needed
222229 FileData = serializedUserDataTable;
223230 }
224231
@@ -248,7 +255,7 @@ namespace GSF.Security · 23 unmodified lines
248255 {
249256 // Decrypt data that was encrypted local to this machine
250257 byte[] serializedUserRoles = ProtectedData.Unprotect(fileStream.ReadStream(), null, DataProtectionScope.LocalMachine);
251− Dictionary<string, string> userRoles = Serialization.Deserialize<Dictionary<string, string>>(serializedUserRoles, SerializationFormat.Binary);
258+ Dictionary<string, string[]> userRoles = Serialization.Deserialize<Dictionary<string, string[]>>(serializedUserRoles, SerializationFormat.Binary);
252259
253260 // Wait for thread level lock on user role dictionary
254261 lock (m_userRolesLock)
@@ -266,7 +273,7 @@ namespace GSF.Security · 11 unmodified lines
266273 /// <param name="userName">User name to hash.</param>
267274 /// <returns>The Base64 encoded calculated SHA-2 hash of the <paramref name="userName"/> used as the key for the user roles dictionary.</returns>
268275 /// <remarks>
269− /// For added security, a hash of the <paramref name="userName"/> is used as the key for access roles
276+ /// For added security, a hash of the <paramref name="userName"/> is used as the key for accessing roles
270277 /// in the user roles cache instead of the actual <paramref name="userName"/>. This method allows the
271278 /// consumer to properly calculate this hash when directly using the user data cache.
272279 /// </remarks>
@@ -324,8 +331,8 @@ namespace GSF.Security · 51 unmodified lines
324331
325332 try
326333 {
327− // Validate that user has write access to the local cryptographic cache folder
328− string tempFile = FilePath.GetDirectoryName(localCacheFileName) + Guid.NewGuid().ToString() + ".tmp";
334+ // Validate that user has write access to the local cache folder
335+ string tempFile = FilePath.GetDirectoryName(localCacheFileName) + Guid.NewGuid() + ".tmp";
329336
330337 using (File.Create(tempFile))
331338 {
@@ -334,13 +341,13 @@ namespace GSF.Security · 2 unmodified lines
334341 if (File.Exists(tempFile))
335342 File.Delete(tempFile);
336343
337− // No access issues exist, use local cache as the primary cryptographic key and initialization vector cache
344+ // No access issues exist, use local cache as the primary cache
338345 currentCache = localUserRoleCache;
339346 currentCache.AutoSave = true;
340347 }
341348 catch (UnauthorizedAccessException)
342349 {
343− // User does not have needed serialization access to common cryptographic cache folder,
350+ // User does not have needed serialization access to common cache folder,
344351 // use a path where user will have rights
345352 string userCacheFolder = FilePath.AddPathSuffix(FilePath.GetApplicationDataFolder());
346353 string userCacheFileName = userCacheFolder + FilePath.GetFileName(localCacheFileName);
@@ -349,11 +356,11 @@ namespace GSF.Security · 2 unmodified lines
349356 if (!Directory.Exists(userCacheFolder))
350357 Directory.CreateDirectory(userCacheFolder);
351358
352− // Copy existing common cryptographic cache if none exists
359+ // Copy existing common cache if none exists
353360 if (File.Exists(localCacheFileName) && !File.Exists(userCacheFileName))
354361 File.Copy(localCacheFileName, userCacheFileName);
355362
356− // Initialize primary cryptographic key and initialization vector cache within user folder
363+ // Initialize primary cache within user folder
357364 currentCache = new UserRoleCache
358365 {
359366 FileName = userCacheFileName,
@@ -367,10 +374,10 @@ namespace GSF.Security · 7 unmodified lines
367374 AutoSave = true
368375 };
369376
370− // Load initial keys
377+ // Load initial roles
371378 currentCache.Load();
372379
373− // Merge new or updated keys, protected folder keys taking precendence over user keys
380+ // Merge new or updated roles, protected folder roles taking precedence over user folder roles
374381 currentCache.MergeRight(localUserRoleCache);
375382 }
376383
Source/Libraries/GSF.TimeSeries/ServiceHostBase.cs+12−0
@@ -52,6 +52,7 @@ using GSF.Data; · 51 unmodified lines
5252 using GSF.IO;
5353 using GSF.Net.Security;
5454 using GSF.Reflection;
55+using GSF.Security;
5556 using GSF.ServiceProcess;
5657 using GSF.TimeSeries.Adapters;
5758 using GSF.Units;
@@ -968,6 +969,17 @@ namespace GSF.TimeSeries · 910 unmodified lines
968969 DisplayStatusMessage("{0} configuration pre-cache completed in {1}.", UpdateType.Information, source.TableName, operationElapsedTime < 0.01D ? "less than a second" : operationElapsedTime.ToString("0.00") + " seconds");
969970 }
970971
972+ DisplayStatusMessage("Preparing current security context...", UpdateType.Information);
973+
974+ operationStartTime = DateTime.UtcNow.Ticks;
975+
976+ // Extract and begin cache of current security context - this does not require an existing security provider
977+ AdoSecurityProvider.PrepareSecurityContext(connection);
978+
979+ operationElapsedTime = (DateTime.UtcNow.Ticks - operationStartTime).ToSeconds();
980+
981+ DisplayStatusMessage("Security context prepared in {1}.", UpdateType.Information, operationElapsedTime < 0.01D ? "less than a second" : operationElapsedTime.ToString("0.00") + " seconds");
982+
971983 DisplayStatusMessage("Database configuration successfully loaded.", UpdateType.Information);
972984 }
973985 catch (Exception ex)

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t