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
124 if ((object)obj == null)124 if ((object)obj == null)
125 throw new ArgumentNullException("obj");125 throw new ArgumentNullException("obj");
126 126
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...
127 foreach (DataTable table in obj.Tables)129 foreach (DataTable table in obj.Tables)
128 {130 {
129 foreach (DataRow row in table.Rows)131 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
37namespace GSF.IO37namespace GSF.IO
38{38{
39 /// <summary>39 /// <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.
41 /// </summary>41 /// </summary>
42 /// <remarks>42 /// <remarks>
43 /// Note that all file data in this class gets serialized to and from memory, as such, the design intention for this class is for43 /// 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.
45 /// </remarks>45 /// </remarks>
46 public class InterprocessCache : IDisposable46 public class InterprocessCache : IDisposable
47 {47 {
@@ -62,18 +62,18 @@ namespace GSF.IO · 14 unmodified lines
62 public const double DefaultRetryDelayInterval = 200.0D;62 public const double DefaultRetryDelayInterval = 200.0D;
63 63
64 // Fields64 // Fields
65 private string m_fileName; // Path and file name of file needing interprocess synchronization65 private string m_fileName; // Path and file name of file needing inter-process synchronization
66 private byte[] m_fileData; // Data loaded or to be saved66 private byte[] m_fileData; // Data loaded or to be saved
67 private bool m_autoSave; // Flag to auto save when file data has changed67 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 access68 private InterprocessReaderWriterLock m_fileLock; // Inter-process reader/writer lock used to synchronize file access
69 private ReaderWriterLockSlim m_dataLock; // Thread level reader/writer lock used to synchronize file data access69 private ReaderWriterLockSlim m_dataLock; // Thread level reader/writer lock used to synchronize file data access
70 private ManualResetEventSlim m_loadIsReady; // Wait handle used so that system will wait for file data load70 private ManualResetEventSlim m_loadIsReady; // Wait handle used so that system will wait for file data load
71 private ManualResetEventSlim m_saveIsReady; // Wait handle used so that system will wait for file data save71 private ManualResetEventSlim m_saveIsReady; // Wait handle used so that system will wait for file data save
72 private FileSystemWatcher m_fileWatcher; // Optional file watcher used to reload changes72 private FileSystemWatcher m_fileWatcher; // Optional file watcher used to reload changes
73 private readonly int m_maximumConcurrentLocks; // Maximum concurrent reader locks allowed73 private readonly int m_maximumConcurrentLocks; // Maximum concurrent reader locks allowed
74 private int m_maximumRetryAttempts; // Maximum retry attempts allowed for loading file74 private int m_maximumRetryAttempts; // Maximum retry attempts allowed for loading file
75 private readonly BitArray m_retryQueue; // Retry event queue75 private readonly BitArray m_retryQueue; // Retry event queue
76 private Timer m_retryTimer; // File I/O retry timer76 private Timer m_retryTimer; // File I/O retry timer
77 private long m_lastRetryTime; // Time of last retry attempt77 private long m_lastRetryTime; // Time of last retry attempt
78 private int m_retryCount; // Total number of retries attempted so far78 private int m_retryCount; // Total number of retries attempted so far
79 private bool m_disposed; // Class disposed flag79 private bool m_disposed; // Class disposed flag
@@ -125,9 +125,9 @@ namespace GSF.IO · 45 unmodified lines
125 #region [ Properties ]125 #region [ Properties ]
126 126
127 /// <summary>127 /// <summary>
128 /// Path and file name for the cache needing interprocess synchronization.128 /// Path and file name for the cache needing inter-process synchronization.
129 /// </summary>129 /// </summary>
130 public virtual string FileName130 public string FileName
131 {131 {
132 get132 get
133 {133 {
@@ -136,7 +136,7 @@ namespace GSF.IO · 2 unmodified lines
136 set136 set
137 {137 {
138 if ((object)value == null)138 if ((object)value == null)
139 throw new ArgumentNullException("FileName", "FileName cannot be null");139 throw new NullReferenceException("FileName cannot be null");
140 140
141 m_fileName = FilePath.GetAbsolutePath(value);141 m_fileName = FilePath.GetAbsolutePath(value);
142 142
@@ -154,7 +154,7 @@ namespace GSF.IO · 11 unmodified lines
154 /// <remarks>154 /// <remarks>
155 /// Setting value to <c>null</c> will create a zero-length file.155 /// Setting value to <c>null</c> will create a zero-length file.
156 /// </remarks>156 /// </remarks>
157 public virtual byte[] FileData157 public byte[] FileData
158 {158 {
159 get159 get
160 {160 {
@@ -179,7 +179,7 @@ namespace GSF.IO · 18 unmodified lines
179 set179 set
180 {180 {
181 if ((object)m_fileName == null)181 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");
183 183
184 bool dataChanged = false;184 bool dataChanged = false;
185 185
@@ -213,7 +213,7 @@ namespace GSF.IO · 27 unmodified lines
213 /// <summary>213 /// <summary>
214 /// Gets or sets flag that determines if <see cref="InterprocessCache"/> should automatically initiate a save when <see cref="FileData"/> has been updated.214 /// Gets or sets flag that determines if <see cref="InterprocessCache"/> should automatically initiate a save when <see cref="FileData"/> has been updated.
215 /// </summary>215 /// </summary>
216 public virtual bool AutoSave216 public bool AutoSave
217 {217 {
218 get218 get
219 {219 {
@@ -228,7 +228,7 @@ namespace GSF.IO · 8 unmodified lines
228 /// <summary>228 /// <summary>
229 /// Gets or sets flag that enables system to monitor for changes in <see cref="FileName"/> and automatically reload <see cref="FileData"/>.229 /// Gets or sets flag that enables system to monitor for changes in <see cref="FileName"/> and automatically reload <see cref="FileData"/>.
230 /// </summary>230 /// </summary>
231 public virtual bool ReloadOnChange231 public bool ReloadOnChange
232 {232 {
233 get233 get
234 {234 {
@@ -239,7 +239,7 @@ namespace GSF.IO · 4 unmodified lines
239 if (value && (object)m_fileWatcher == null)239 if (value && (object)m_fileWatcher == null)
240 {240 {
241 if ((object)m_fileName == null)241 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");
243 243
244 // Setup file watcher to monitor for external updates244 // Setup file watcher to monitor for external updates
245 m_fileWatcher = new FileSystemWatcher();245 m_fileWatcher = new FileSystemWatcher();
@@ -261,7 +261,7 @@ namespace GSF.IO · 15 unmodified lines
261 /// <summary>261 /// <summary>
262 /// Gets the maximum concurrent reader locks allowed.262 /// Gets the maximum concurrent reader locks allowed.
263 /// </summary>263 /// </summary>
264 public virtual int MaximumConcurrentLocks264 public int MaximumConcurrentLocks
265 {265 {
266 get266 get
267 {267 {
@@ -272,7 +272,7 @@ namespace GSF.IO · 4 unmodified lines
272 /// <summary>272 /// <summary>
273 /// Maximum retry attempts allowed for loading or saving cache file data.273 /// Maximum retry attempts allowed for loading or saving cache file data.
274 /// </summary>274 /// </summary>
275 public virtual int MaximumRetryAttempts275 public int MaximumRetryAttempts
276 {276 {
277 get277 get
278 {278 {
@@ -287,7 +287,7 @@ namespace GSF.IO · 8 unmodified lines
287 /// <summary>287 /// <summary>
288 /// Wait interval, in milliseconds, before retrying load or save of cache file data.288 /// Wait interval, in milliseconds, before retrying load or save of cache file data.
289 /// </summary>289 /// </summary>
290 public virtual double RetryDelayInterval290 public double RetryDelayInterval
291 {291 {
292 get292 get
293 {293 {
@@ -368,7 +368,7 @@ namespace GSF.IO · 74 unmodified lines
368 }368 }
369 369
370 /// <summary>370 /// <summary>
371 /// Initiates interprocess synchronized cache file save.371 /// Initiates inter-process synchronized cache file save.
372 /// </summary>372 /// </summary>
373 /// <remarks>373 /// <remarks>
374 /// Subclasses should always call <see cref="WaitForLoad()"/> before calling this method.374 /// Subclasses should always call <see cref="WaitForLoad()"/> before calling this method.
@@ -376,17 +376,17 @@ namespace GSF.IO · 1 unmodified line
376 public virtual void Save()376 public virtual void Save()
377 {377 {
378 if ((object)m_fileName == null)378 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");
380 380
381 if ((object)m_fileData == null)381 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");
383 383
384 m_saveIsReady.Reset();384 m_saveIsReady.Reset();
385 ThreadPool.QueueUserWorkItem(SynchronizedWrite);385 ThreadPool.QueueUserWorkItem(SynchronizedWrite);
386 }386 }
387 387
388 /// <summary>388 /// <summary>
389 /// Initiates interprocess synchronized cache file load.389 /// Initiates inter-process synchronized cache file load.
390 /// </summary>390 /// </summary>
391 /// <remarks>391 /// <remarks>
392 /// Subclasses should always call <see cref="WaitForLoad()"/> before calling this method.392 /// Subclasses should always call <see cref="WaitForLoad()"/> before calling this method.
@@ -394,7 +394,7 @@ namespace GSF.IO · 1 unmodified line
394 public virtual void Load()394 public virtual void Load()
395 {395 {
396 if ((object)m_fileName == null)396 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");
398 398
399 m_loadIsReady.Reset();399 m_loadIsReady.Reset();
400 ThreadPool.QueueUserWorkItem(SynchronizedRead);400 ThreadPool.QueueUserWorkItem(SynchronizedRead);
@@ -471,58 +471,58 @@ namespace GSF.IO · 70 unmodified lines
471 {471 {
472 try472 try
473 {473 {
474 if (!m_disposed)474 if (m_disposed)
475 return;
476
477 if (m_fileLock.TryEnterWriteLock((int)m_retryTimer.Interval))
475 {478 {
476 if (m_fileLock.TryEnterWriteLock((int)m_retryTimer.Interval))479 FileStream fileStream = null;
480
481 try
477 {482 {
478 FileStream fileStream = null;483 fileStream = new FileStream(m_fileName, FileMode.Create, FileAccess.Write, FileShare.None);
479 484
480 try485 if (m_dataLock.TryEnterReadLock((int)m_retryTimer.Interval))
481 {486 {
482 fileStream = new FileStream(m_fileName, FileMode.Create, FileAccess.Write, FileShare.None);487 try
483
484 if (m_dataLock.TryEnterReadLock((int)m_retryTimer.Interval))
485 {488 {
486 try489 // Disable file watch notification before update
487 {490 if ((object)m_fileWatcher != null)
488 // Disable file watch notification before update491 m_fileWatcher.EnableRaisingEvents = false;
489 if ((object)m_fileWatcher != null)
490 m_fileWatcher.EnableRaisingEvents = false;
491 492
492 SaveFileData(fileStream, m_fileData);493 SaveFileData(fileStream, m_fileData);
493 m_saveIsReady.Set();494 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 }
503 }495 }
504 else496 finally
505 {497 {
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;
507 }503 }
508 }504 }
509 catch (IOException ex)505 else
510 {
511 RetrySynchronizedEvent(ex, WriteEvent);
512 }
513 finally
514 {506 {
515 m_fileLock.ExitWriteLock();507 RetrySynchronizedEvent(new TimeoutException("Timeout waiting to acquire read lock for local cache"), WriteEvent);
516
517 if ((object)fileStream != null)
518 fileStream.Close();
519 }508 }
520 }509 }
521 else510 catch (IOException ex)
522 {511 {
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();
524 }520 }
525 }521 }
522 else
523 {
524 RetrySynchronizedEvent(new TimeoutException("Timeout waiting to acquire write lock for " + m_fileName), WriteEvent);
525 }
526 }526 }
527 catch (ThreadAbortException)527 catch (ThreadAbortException)
528 {528 {
@@ -542,63 +542,63 @@ namespace GSF.IO · 13 unmodified lines
542 {542 {
543 try543 try
544 {544 {
545 if (!m_disposed)545 if (m_disposed)
546 return;
547
548 if (File.Exists(m_fileName))
546 {549 {
547 if (File.Exists(m_fileName))550 if (m_fileLock.TryEnterReadLock((int)m_retryTimer.Interval))
548 {551 {
549 if (m_fileLock.TryEnterReadLock((int)m_retryTimer.Interval))552 FileStream fileStream = null;
553
554 try
550 {555 {
551 FileStream fileStream = null;556 fileStream = new FileStream(m_fileName, FileMode.Open, FileAccess.Read, FileShare.Read);
552 557
553 try558 if (m_dataLock.TryEnterWriteLock((int)m_retryTimer.Interval))
554 {559 {
555 fileStream = new FileStream(m_fileName, FileMode.Open, FileAccess.Read, FileShare.Read);560 try
556
557 if (m_dataLock.TryEnterWriteLock((int)m_retryTimer.Interval))
558 {561 {
559 try562 m_fileData = LoadFileData(fileStream);
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();
570 }563 }
571 else564 finally
572 {565 {
573 RetrySynchronizedEvent(new TimeoutException("Timeout waiting to acquire write lock for local cache"), ReadEvent);566 m_dataLock.ExitWriteLock();
574 }567 }
568
569 // Release any threads waiting for file data
570 m_loadIsReady.Set();
575 }571 }
576 catch (IOException ex)572 else
577 {
578 RetrySynchronizedEvent(ex, ReadEvent);
579 }
580 finally
581 {573 {
582 m_fileLock.ExitReadLock();574 RetrySynchronizedEvent(new TimeoutException("Timeout waiting to acquire write lock for local cache"), ReadEvent);
583
584 if ((object)fileStream != null)
585 fileStream.Close();
586 }575 }
587 }576 }
588 else577 catch (IOException ex)
589 {578 {
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();
591 }587 }
592 }588 }
593 else589 else
594 {590 {
595 // File doesn't exist, create ane empty array representing a zero-length file591 RetrySynchronizedEvent(new TimeoutException("Timeout waiting to acquire read lock for " + m_fileName), ReadEvent);
596 m_fileData = new byte[0];
597
598 // Release any threads waiting for file data
599 m_loadIsReady.Set();
600 }592 }
601 }593 }
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 }
602 }602 }
603 catch (ThreadAbortException)603 catch (ThreadAbortException)
604 {604 {
@@ -618,34 +618,35 @@ namespace GSF.IO · 13 unmodified lines
618 /// <param name="eventType">Event type to retry.</param>618 /// <param name="eventType">Event type to retry.</param>
619 private void RetrySynchronizedEvent(Exception ex, int eventType)619 private void RetrySynchronizedEvent(Exception ex, int eventType)
620 {620 {
621 if (m_disposed)
622 return;
623
621 // A retry is only being initiating for basic file I/O or locking errors, all other errors will initiate an unhandled624 // A retry is only being initiating for basic file I/O or locking errors, all other errors will initiate an unhandled
622 // exception causing system exit. It would be an error, IMO, for the system to create values then not be able to load625 // exception causing system exit. It would be an error, IMO, for the system to create values then not be able to load
623 // them at next run or not be able to use values from last run because file could not be loaded.626 // 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))
625 {630 {
626 // We monitor basic I/O and lock failures occurring in quick succession, we can't allow retry activity to go on forever631 // Significant time has passed since last retry, so we reset counter
627 if (DateTime.UtcNow.Ticks - m_lastRetryTime > (long)Ticks.FromMilliseconds(m_retryTimer.Interval * m_maximumRetryAttempts))632 m_retryCount = 0;
628 {633 m_lastRetryTime = DateTime.UtcNow.Ticks;
629 // Significant time has passed since last retry, so we reset counter634 }
630 m_retryCount = 0;635 else
631 m_lastRetryTime = DateTime.UtcNow.Ticks;636 {
632 }637 m_retryCount++;
633 else
634 {
635 m_retryCount++;
636 638
637 if (m_retryCount >= m_maximumRetryAttempts)639 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);640 throw new UnauthorizedAccessException("Failed to " + (eventType == WriteEvent ? "write data to " : "read data from ") + m_fileName + " after " + m_maximumRetryAttempts + " attempts: " + ex.Message, ex);
639 }641 }
640 642
641 // Technically the interprocess mutex will handle serialized access to the file, but if the OS or other process643 // Technically the inter-process 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.644 // 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)645 lock (m_retryQueue)
644 {646 {
645 m_retryQueue[eventType] = true;647 m_retryQueue[eventType] = true;
646 }
647 m_retryTimer.Start();
648 }648 }
649 m_retryTimer.Start();
649 }650 }
650 651
651 /// <summary>652 /// <summary>
@@ -653,34 +654,34 @@ namespace GSF.IO · 1 unmodified line
653 /// </summary>654 /// </summary>
654 private void m_retryTimer_Elapsed(object sender, ElapsedEventArgs e)655 private void m_retryTimer_Elapsed(object sender, ElapsedEventArgs e)
655 {656 {
656 if (!m_disposed)657 if (m_disposed)
657 {658 return;
658 WaitCallback callBackEvent = null;
659 659
660 lock (m_retryQueue)660 WaitCallback callBackEvent = null;
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 }
675 661
676 // If any events remain queued for retry, start timer for next event662 lock (m_retryQueue)
677 if (m_retryQueue.Any(true))663 {
678 m_retryTimer.Start();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;
679 }676 }
680 677
681 if ((object)callBackEvent != null)678 // If any events remain queued for retry, start timer for next event
682 ThreadPool.QueueUserWorkItem(callBackEvent);679 if (m_retryQueue.Any(true))
680 m_retryTimer.Start();
683 }681 }
682
683 if ((object)callBackEvent != null)
684 ThreadPool.QueueUserWorkItem(callBackEvent);
684 }685 }
685 686
686 /// <summary>687 /// <summary>
Source/Libraries/GSF.Core/Security/Cryptography/Cipher.cs+3−7
@@ -428,7 +428,7 @@ namespace GSF.Security.Cryptography · 427 unmodified lines
428 static Cipher()428 static Cipher()
429 {429 {
430 const string fipsKeyOld = "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Lsa";430 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";
432 432
433 KeyIVCache localKeyIVCache;433 KeyIVCache localKeyIVCache;
434 string localCacheFileName = DefaultCacheFileName;434 string localCacheFileName = DefaultCacheFileName;
@@ -447,7 +447,7 @@ namespace GSF.Security.Cryptography · 12 unmodified lines
447 retryDelayInterval = settings["CacheRetryDelayInterval"].ValueAs(retryDelayInterval);447 retryDelayInterval = settings["CacheRetryDelayInterval"].ValueAs(retryDelayInterval);
448 maximumRetryAttempts = settings["CacheMaximumRetryAttempts"].ValueAs(maximumRetryAttempts);448 maximumRetryAttempts = settings["CacheMaximumRetryAttempts"].ValueAs(maximumRetryAttempts);
449 449
450 // Determine if the user needs to use FIPS-compliant algorithms.450 // Determine if the user needs to use FIPS-compliant algorithms
451 s_managedEncryption = (Registry.GetValue(fipsKeyNew, "Enabled", 0) ?? Registry.GetValue(fipsKeyOld, "FIPSAlgorithmPolicy", 0)).ToString() == "0";451 s_managedEncryption = (Registry.GetValue(fipsKeyNew, "Enabled", 0) ?? Registry.GetValue(fipsKeyOld, "FIPSAlgorithmPolicy", 0)).ToString() == "0";
452 452
453 // Initialize local cryptographic key and initialization vector cache (application may only have read-only access to this cache)453 // 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
473 try473 try
474 {474 {
475 // Validate that user has write access to the local cryptographic cache folder475 // Validate that user has write access to the local cryptographic cache folder
476#if MONO476 string tempFile = FilePath.GetDirectoryName(localCacheFileName) + Guid.NewGuid() + ".tmp";
477 string tempFile = FilePath.GetDirectoryName(localCacheFileName) + Guid.NewGuid().ToString() + ".tmp";
478 477
479 using (File.Create(tempFile))478 using (File.Create(tempFile))
480 {479 {
@@ -482,9 +481,6 @@ namespace GSF.Security.Cryptography · 1 unmodified line
482 481
483 if (File.Exists(tempFile))482 if (File.Exists(tempFile))
484 File.Delete(tempFile);483 File.Delete(tempFile);
485#else
486 Directory.GetAccessControl(FilePath.GetDirectoryName(localCacheFileName));
487#endif
488 484
489 // No access issues exist, use local cache as the primary cryptographic key and initialization vector cache485 // No access issues exist, use local cache as the primary cryptographic key and initialization vector cache
490 s_keyIVCache = localKeyIVCache;486 s_keyIVCache = localKeyIVCache;
Source/Libraries/GSF.Core/Threading/InterprocessLock.cs+2−2
@@ -87,7 +87,7 @@ namespace GSF.Threading · 86 unmodified lines
87 // Create a mutex name that is specific to an object (e.g., a path and file name).87 // Create a mutex name that is specific to an object (e.g., a path and file name).
88 // Note that we use GetPasswordHash to create a short common name for the name parameter88 // Note that we use GetPasswordHash to create a short common name for the name parameter
89 // that was passed into the function - this allows the parameter to be very long, e.g.,89 // 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.
91 91
92 // Prefix mutex name with "Global\" such that mutex will apply to all active92 // Prefix mutex name with "Global\" such that mutex will apply to all active
93 // application sessions in case terminal services is running.93 // application sessions in case terminal services is running.
@@ -211,7 +211,7 @@ namespace GSF.Threading · 117 unmodified lines
211 catch (UnauthorizedAccessException)211 catch (UnauthorizedAccessException)
212 {212 {
213 // Named semaphore exists now but current user doesn't have full control, attempt to open with minimum needed rights213 // 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);
215 }215 }
216 }216 }
217#endif217#endif
Source/Libraries/GSF.Core/Threading/InterprocessReaderWriterLock.cs+24−16
@@ -29,7 +29,7 @@ using System.Threading; · 28 unmodified lines
29namespace GSF.Threading29namespace GSF.Threading
30{30{
31 /// <summary>31 /// <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.
33 /// </summary>33 /// </summary>
34 public class InterprocessReaderWriterLock : IDisposable34 public class InterprocessReaderWriterLock : IDisposable
35 {35 {
@@ -43,9 +43,9 @@ namespace GSF.Threading · 7 unmodified lines
43 public const int DefaultMaximumConcurrentLocks = 10;43 public const int DefaultMaximumConcurrentLocks = 10;
44 44
45 // Fields45 // Fields
46 private Mutex m_semaphoreLock; // Mutex used to synchronize access to Semaphore46 private Mutex m_semaphoreLock; // Mutex used to synchronize access to Semaphore
47 private Semaphore m_concurrencyLock; // Semaphore used for reader/writer lock on consumer object47 private Semaphore m_concurrencyLock; // Semaphore used for reader/writer lock on consumer object
48 private readonly int m_maximumConcurrentLocks;48 private readonly int m_maximumConcurrentLocks; // Maximum number of concurrent locks before waiting
49 private bool m_disposed;49 private bool m_disposed;
50 50
51 #endregion51 #endregion
@@ -56,7 +56,7 @@ namespace GSF.Threading · 4 unmodified lines
56 /// Creates a new instance of the <see cref="InterprocessReaderWriterLock"/> associated with the specified56 /// Creates a new instance of the <see cref="InterprocessReaderWriterLock"/> associated with the specified
57 /// <paramref name="name"/> that identifies a source object needing concurrency locking.57 /// <paramref name="name"/> that identifies a source object needing concurrency locking.
58 /// </summary>58 /// </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>
60 public InterprocessReaderWriterLock(string name)60 public InterprocessReaderWriterLock(string name)
61 : this(name, DefaultMaximumConcurrentLocks)61 : this(name, DefaultMaximumConcurrentLocks)
62 {62 {
@@ -66,7 +66,7 @@ namespace GSF.Threading · 3 unmodified lines
66 /// Creates a new instance of the <see cref="InterprocessReaderWriterLock"/> associated with the specified66 /// Creates a new instance of the <see cref="InterprocessReaderWriterLock"/> associated with the specified
67 /// <paramref name="name"/> that identifies a source object needing concurrency locking.67 /// <paramref name="name"/> that identifies a source object needing concurrency locking.
68 /// </summary>68 /// </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>
70 /// <param name="maximumConcurrentLocks">Maximum concurrent reader locks to allow.</param>70 /// <param name="maximumConcurrentLocks">Maximum concurrent reader locks to allow.</param>
71 /// <remarks>71 /// <remarks>
72 /// If more reader locks are requested than the <paramref name="maximumConcurrentLocks"/>, excess reader locks will simply72 /// 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
128 if (disposing)128 if (disposing)
129 {129 {
130 if ((object)m_concurrencyLock != null)130 if ((object)m_concurrencyLock != null)
131 {
131 m_concurrencyLock.Close();132 m_concurrencyLock.Close();
132 m_concurrencyLock = null;133 m_concurrencyLock = null;
134 }
133 135
134 if ((object)m_semaphoreLock != null)136 if ((object)m_semaphoreLock != null)
137 {
135 m_semaphoreLock.Close();138 m_semaphoreLock.Close();
136 m_semaphoreLock = null;139 m_semaphoreLock = null;
140 }
137 }141 }
138 }142 }
139 finally143 finally
@@ -147,7 +151,7 @@ namespace GSF.Threading · 7 unmodified lines
147 /// Tries to enter the lock in read mode.151 /// Tries to enter the lock in read mode.
148 /// </summary>152 /// </summary>
149 /// <remarks>153 /// <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"/>.
151 /// One <see cref="ExitReadLock"/> should be called for each <see cref="EnterReadLock"/> or <see cref="TryEnterReadLock"/>.155 /// One <see cref="ExitReadLock"/> should be called for each <see cref="EnterReadLock"/> or <see cref="TryEnterReadLock"/>.
152 /// </remarks>156 /// </remarks>
153 public void EnterReadLock()157 public void EnterReadLock()
@@ -159,7 +163,7 @@ namespace GSF.Threading · 5 unmodified lines
159 /// Tries to enter the lock in write mode.163 /// Tries to enter the lock in write mode.
160 /// </summary>164 /// </summary>
161 /// <remarks>165 /// <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"/>.
163 /// One <see cref="ExitWriteLock"/> should be called for each <see cref="EnterWriteLock"/> or <see cref="TryEnterWriteLock"/>.167 /// One <see cref="ExitWriteLock"/> should be called for each <see cref="EnterWriteLock"/> or <see cref="TryEnterWriteLock"/>.
164 /// </remarks>168 /// </remarks>
165 public void EnterWriteLock()169 public void EnterWriteLock()
@@ -171,7 +175,7 @@ namespace GSF.Threading · 5 unmodified lines
171 /// Exits read mode and returns the prior read lock count.175 /// Exits read mode and returns the prior read lock count.
172 /// </summary>176 /// </summary>
173 /// <remarks>177 /// <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"/>.
175 /// One <see cref="ExitReadLock"/> should be called for each <see cref="EnterReadLock"/> or <see cref="TryEnterReadLock"/>.179 /// One <see cref="ExitReadLock"/> should be called for each <see cref="EnterReadLock"/> or <see cref="TryEnterReadLock"/>.
176 /// </remarks>180 /// </remarks>
177 public int ExitReadLock()181 public int ExitReadLock()
@@ -184,7 +188,7 @@ namespace GSF.Threading · 6 unmodified lines
184 /// Exits write mode.188 /// Exits write mode.
185 /// </summary>189 /// </summary>
186 /// <remarks>190 /// <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"/>.
188 /// One <see cref="ExitWriteLock"/> should be called for each <see cref="EnterWriteLock"/> or <see cref="TryEnterWriteLock"/>.192 /// One <see cref="ExitWriteLock"/> should be called for each <see cref="EnterWriteLock"/> or <see cref="TryEnterWriteLock"/>.
189 /// </remarks>193 /// </remarks>
190 public void ExitWriteLock()194 public void ExitWriteLock()
@@ -199,12 +203,12 @@ namespace GSF.Threading · 8 unmodified lines
199 /// <param name="millisecondsTimeout">The number of milliseconds to wait, or -1 (<see cref="Timeout.Infinite"/>) to wait indefinitely.</param>203 /// <param name="millisecondsTimeout">The number of milliseconds to wait, or -1 (<see cref="Timeout.Infinite"/>) to wait indefinitely.</param>
200 /// <returns><c>true</c> if the calling thread entered read mode, otherwise, <c>false</c>.</returns>204 /// <returns><c>true</c> if the calling thread entered read mode, otherwise, <c>false</c>.</returns>
201 /// <remarks>205 /// <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"/>.
203 /// One <see cref="ExitReadLock"/> should be called for each <see cref="EnterReadLock"/> or <see cref="TryEnterReadLock"/>.207 /// One <see cref="ExitReadLock"/> should be called for each <see cref="EnterReadLock"/> or <see cref="TryEnterReadLock"/>.
204 /// </remarks>208 /// </remarks>
205 public bool TryEnterReadLock(int millisecondsTimeout)209 public bool TryEnterReadLock(int millisecondsTimeout)
206 {210 {
207 bool success = false;211 bool success;
208 212
209 try213 try
210 {214 {
@@ -263,7 +267,9 @@ namespace GSF.Threading · 52 unmodified lines
263 // The only way to get a semaphore slot count is to execute a successful wait and release.267 // The only way to get a semaphore slot count is to execute a successful wait and release.
264 startTime = DateTime.UtcNow.Ticks;268 startTime = DateTime.UtcNow.Ticks;
265 269
266 if (success = m_concurrencyLock.WaitOne(millisecondsTimeout))270 success = m_concurrencyLock.WaitOne(millisecondsTimeout);
271
272 if (success)
267 {273 {
268 int count = m_concurrencyLock.Release();274 int count = m_concurrencyLock.Release();
269 int adjustedTimeout = millisecondsTimeout;275 int adjustedTimeout = millisecondsTimeout;
@@ -282,7 +288,9 @@ namespace GSF.Threading · 12 unmodified lines
282 if (adjustedTimeout < 0)288 if (adjustedTimeout < 0)
283 adjustedTimeout = 0;289 adjustedTimeout = 0;
284 290
285 if (success = m_concurrencyLock.WaitOne(adjustedTimeout))291 success = m_concurrencyLock.WaitOne(adjustedTimeout);
292
293 if (success)
286 count = m_concurrencyLock.Release();294 count = m_concurrencyLock.Release();
287 }295 }
288 }296 }
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
24using System;
25using System.Data;
26using System.IO;
27using System.Security.Cryptography;
28using GSF.Configuration;
29using GSF.Data;
30using GSF.IO;
31using GSF.Threading;
32
33namespace 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
27// Modified database queries to work with Oracle database.27// Modified database queries to work with Oracle database.
28// 12/20/2012 - Starlynn Danyelle Gilliam28// 12/20/2012 - Starlynn Danyelle Gilliam
29// Modified Header.29// 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.
30//33//
31//******************************************************************************************************34//******************************************************************************************************
32 35
33using System;36using System;
37using System.Collections.Generic;
34using System.Configuration;38using System.Configuration;
35using System.Data;39using System.Data;
36using System.Diagnostics;40using System.Diagnostics;
@@ -39,17 +43,19 @@ using System.Security; · 2 unmodified lines
39using System.Text.RegularExpressions;43using System.Text.RegularExpressions;
40using System.Threading;44using System.Threading;
41using System.Web.Security;45using System.Web.Security;
46using GSF.Collections;
42using GSF.Configuration;47using GSF.Configuration;
43using GSF.Data;48using GSF.Data;
49using GSF.Identity;
44 50
45namespace GSF.Security51namespace GSF.Security
46{52{
47 /// <summary>53 /// <summary>
48 /// Represents an <see cref="ISecurityProvider"/> that uses ADO.NET data source (SQL Server, MySQL, Microsoft Access etc) for its54 /// Represents an <see cref="ISecurityProvider"/> that uses ADO.NET data source (SQL Server, MySQL, Oracle, etc.) for its
49 /// backend datastore and authenticates internal users against Active Directory and external users against the database.55 /// backend data store and authenticates internal users against Active Directory and external users against the database.
50 /// </summary>56 /// </summary>
51 /// <example>57 /// <example>
52 /// Required config file entries:58 /// Required config file entries (automatically added):
53 /// <code>59 /// <code>
54 /// <![CDATA[60 /// <![CDATA[
55 /// <?xml version="1.0"?>61 /// <?xml version="1.0"?>
@@ -88,14 +94,84 @@ namespace GSF.Security · 32 unmodified lines
88 /// ]]>94 /// ]]>
89 /// </code>95 /// </code>
90 /// </example>96 /// </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>
91 public class AdoSecurityProvider : LdapSecurityProvider154 public class AdoSecurityProvider : LdapSecurityProvider
92 {155 {
93 #region [ Members ]156 #region [ Members ]
94 157
95 // Constants158 // Constants
96 private const int MinimumPasswordLength = 8;159 private const string UserAccountTable = "UserAccount"; // Table name for user accounts
97 private const string PasswordRequirementRegex = "^.*(?=.{8,})(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).*$";160 private const string SecurityGroupTable = "SecurityGroup"; // Table name for security groups
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";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";
99 175
100 /// <summary>176 /// <summary>
101 /// Defines the provider ID for the <see cref="AdoSecurityProvider"/>.177 /// Defines the provider ID for the <see cref="AdoSecurityProvider"/>.
@@ -104,6 +180,8 @@ namespace GSF.Security · 2 unmodified lines
104 180
105 private Exception m_lastException;181 private Exception m_lastException;
106 private bool m_successfulPassThroughAuthentication;182 private bool m_successfulPassThroughAuthentication;
183 private string m_passwordRequirementsRegex;
184 private string m_passwordRequirementsError;
107 185
108 #endregion186 #endregion
109 187
@@ -122,14 +200,17 @@ namespace GSF.Security · 12 unmodified lines
122 /// Initializes a new instance of the <see cref="AdoSecurityProvider"/> class.200 /// Initializes a new instance of the <see cref="AdoSecurityProvider"/> class.
123 /// </summary>201 /// </summary>
124 /// <param name="username">Name that uniquely identifies the user.</param>202 /// <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>203 /// <param name="canRefreshData">true if the security provider can refresh <see cref="UserData"/> from the backend data store, otherwise false.</param>
126 /// <param name="canUpdateData">true if the security provider can update <see cref="UserData"/> in the backend datastore, 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>
127 /// <param name="canResetPassword">true if the security provider can reset user password, otherwise false.</param>205 /// <param name="canResetPassword">true if the security provider can reset user password, otherwise false.</param>
128 /// <param name="canChangePassword">true if the security provider can change user password, otherwise false.</param>206 /// <param name="canChangePassword">true if the security provider can change user password, otherwise false.</param>
129 protected AdoSecurityProvider(string username, bool canRefreshData, bool canUpdateData, bool canResetPassword, bool canChangePassword)207 protected AdoSecurityProvider(string username, bool canRefreshData, bool canUpdateData, bool canResetPassword, bool canChangePassword)
130 : base(username, canRefreshData, canUpdateData, canResetPassword, canChangePassword)208 : base(username, canRefreshData, canUpdateData, canResetPassword, canChangePassword)
131 {209 {
132 base.ConnectionString = "Eval(systemSettings.ConnectionString)";210 base.ConnectionString = "Eval(systemSettings.ConnectionString)";
211
212 m_passwordRequirementsRegex = DefaultPasswordRequirementsRegex;
213 m_passwordRequirementsError = DefaultPasswordRequirementsError;
133 }214 }
134 215
135 #endregion216 #endregion
@@ -137,22 +218,18 @@ namespace GSF.Security · 1 unmodified line
137 #region [ Properties ]218 #region [ Properties ]
138 219
139 /// <summary>220 /// <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.
141 /// </summary>222 /// </summary>
142 public override bool CanUpdateData223 public override bool CanUpdateData
143 {224 {
144 get225 get
145 {226 {
227 // Data update supported on external user accounts.
146 if (UserData.IsDefined && UserData.IsExternal)228 if (UserData.IsDefined && UserData.IsExternal)
147 {
148 // Data update supported on external user accounts.
149 return true;229 return true;
150 }230
151 else231 // Data update not supported on internal user accounts.
152 {232 return false;
153 // Data update not supported on internal user accounts.
154 return false;
155 }
156 }233 }
157 }234 }
158 235
@@ -186,8 +263,14 @@ namespace GSF.Security · 27 unmodified lines
186 // Make sure default settings exist263 // Make sure default settings exist
187 ConfigurationFile config = ConfigurationFile.Current;264 ConfigurationFile config = ConfigurationFile.Current;
188 CategorizedSettingsElementCollection settings = config.Settings[SettingsCategory];265 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.");
190 settings.Add("LdapPath", "", "Specifies the LDAP path used to initialize the security provider.");268 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);
191 }274 }
192 275
193 /// <summary>276 /// <summary>
@@ -208,152 +291,210 @@ namespace GSF.Security · 14 unmodified lines
208 // Table1: Information about the user.291 // Table1: Information about the user.
209 // Table2: Groups the user is a member of.292 // Table2: Groups the user is a member of.
210 // Table3: Roles that are assigned to the user either implicitly (NT groups) or explicitly (database) or through a group.293 // 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;
223 294
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;
225 296
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))
227 {301 {
228 userDataTable.Load(reader);302 securityContext = ExtractSecurityContext(database.Connection);
229 }303 }
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 }
230 310
231 if (userDataTable.Rows.Count <= 0)311 // Validate that needed security tables exist in the data set
232 {312 const string SecurityContextError = "Failed to load a valid security context. Cannot proceed with user authentication.";
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 }
244 313
245 if (UserData.IsExternal && (object)userDataRow != null)314 if ((object)securityContext == null)
246 {315 throw new SecurityException(SecurityContextError);
247 if (string.IsNullOrEmpty(UserData.LoginID))
248 UserData.LoginID = UserData.Username;
249 316
250 if (!Convert.IsDBNull(userDataRow["Password"]))317 foreach (string securityTable in s_securityTables)
251 UserData.Password = Convert.ToString(userDataRow["Password"]);318 {
319 if (!securityContext.Tables.Contains(securityTable))
320 throw new SecurityException(SecurityContextError);
321 }
252 322
253 if (!Convert.IsDBNull(userDataRow["FirstName"]))323 DataRow userAccount = null;
254 UserData.FirstName = Convert.ToString(userDataRow["FirstName"]);324 Guid userAccountID = Guid.Empty;
325 string userSID = UserInfo.AccountNameToSID(UserData.Username);
255 326
256 if (!Convert.IsDBNull(userDataRow["LastName"]))327 // Filter user account data for the current user
257 UserData.LastName = Convert.ToString(userDataRow["LastName"]);328 DataRow[] userAccounts = securityContext.Tables[UserAccountTable].Select(string.Format("Name = '{0}'", userSID));
258 329
259 if (!Convert.IsDBNull(userDataRow["Phone"]))330 if (userAccounts.Length == 0)
260 UserData.PhoneNumber = Convert.ToString(userDataRow["Phone"]);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 }
261 345
262 if (!Convert.IsDBNull(userDataRow["Email"]))346 if (UserData.IsExternal && (object)userAccount != null)
263 UserData.EmailAddress = Convert.ToString(userDataRow["Email"]);347 {
348 // Load database user details
349 if (string.IsNullOrEmpty(UserData.LoginID))
350 UserData.LoginID = UserData.Username;
264 351
265 if (!Convert.IsDBNull(userDataRow["ChangePasswordOn"]))352 if (!Convert.IsDBNull(userAccount["Password"]))
266 UserData.PasswordChangeDateTime = Convert.ToDateTime(userDataRow["ChangePasswordOn"]);353 UserData.Password = Convert.ToString(userAccount["Password"]);
267 354
268 if (!Convert.IsDBNull(userDataRow["CreatedOn"]))355 if (!Convert.IsDBNull(userAccount["FirstName"]))
269 UserData.AccountCreatedDateTime = Convert.ToDateTime(userDataRow["CreatedOn"]);356 UserData.FirstName = Convert.ToString(userAccount["FirstName"]);
270 357
271 // For possible future use:358 if (!Convert.IsDBNull(userAccount["LastName"]))
272 //if (!Convert.IsDBNull(userDataRow["UserCompanyName"]))359 UserData.LastName = Convert.ToString(userAccount["LastName"]);
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 }
286 360
287 // Administrator can lock out NT user as well as database-only user via database361 if (!Convert.IsDBNull(userAccount["Phone"]))
288 if (!UserData.IsLockedOut && userDataRow != null && !Convert.IsDBNull(userDataRow["LockedOut"]))362 UserData.PhoneNumber = Convert.ToString(userAccount["Phone"]);
289 UserData.IsLockedOut = Convert.ToBoolean(userDataRow["LockedOut"]);
290 363
291 // Load explicitly assigned groups364 if (!Convert.IsDBNull(userAccount["Email"]))
292 query = database.ParameterizedQueryString("SELECT SecurityGroupID, SecurityGroupName, SecurityGroupDescription FROM SecurityGroupUserAccountDetail WHERE UserName = {0}", "name");365 UserData.EmailAddress = Convert.ToString(userAccount["Email"]);
293 366
294 using (IDataReader reader = dbConnection.ExecuteReader(query, UserData.Username))367 if (!Convert.IsDBNull(userAccount["ChangePasswordOn"]))
295 {368 UserData.PasswordChangeDateTime = Convert.ToDateTime(userAccount["ChangePasswordOn"]);
296 userGroupDataTable.Load(reader);
297 }
298 369
299 foreach (DataRow group in userGroupDataTable.Rows)370 if (!Convert.IsDBNull(userAccount["CreatedOn"]))
300 {371 UserData.AccountCreatedDateTime = Convert.ToDateTime(userAccount["CreatedOn"]);
301 if (!Convert.IsDBNull(group["SecurityGroupName"]))
302 {
303 groupName = Convert.ToString(group["SecurityGroupName"]);
304 372
305 if (!UserData.Groups.Contains(groupName, StringComparer.InvariantCultureIgnoreCase))373 // For possible future use:
306 UserData.Groups.Add(groupName);374 //if (!Convert.IsDBNull(userDataRow["UserCompanyName"]))
307 }375 // UserData.CompanyName = Convert.ToString(userDataRow["UserCompanyName"]);
308 }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 }
309 388
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"]);
311 392
312 // Load explicitly assigned roles393 // 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
313 query = database.ParameterizedQueryString("SELECT ApplicationRoleID, ApplicationRoleName, ApplicationRoleDescription FROM AppRoleUserAccountDetail WHERE UserName = {0}", "name");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.
314 398
315 using (IDataReader reader = dbConnection.ExecuteReader(query, UserData.Username))399 if (userAccountID != Guid.Empty)
316 {400 {
317 userRoleDataTable.Load(reader);401 // Filter explicitly assigned security groups for current user
318 }402 DataRow[] userGroups = securityContext.Tables[SecurityGroupUserAccountTable].Select(string.Format("UserAccountID = '{0}'", userAccountID));
319 403
320 if (userRoleDataTable.Rows.Count == 0)404 foreach (DataRow row in userGroups)
321 {405 {
322 // Load implicitly assigned roles406 // Locate associated security group record
323 query = database.ParameterizedQueryString("SELECT ApplicationRoleID, ApplicationRoleName, ApplicationRoleDescription FROM AppRoleSecurityGroupDetail WHERE SecurityGroupName = {0}", "groupName");407 DataRow[] securityGroups = securityContext.Tables[SecurityGroupTable].Select(string.Format("ID = '{0}'", row["SecurityGroupID"]));
324 408
325 foreach (string group in UserData.Groups)409 if (securityGroups.Length > 0)
326 {410 {
327 using (IDataReader reader = dbConnection.ExecuteReader(query, group))411 DataRow securityGroup = securityGroups[0];
412
413 if (!Convert.IsDBNull(securityGroup["Name"]))
328 {414 {
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);
330 }423 }
331 }424 }
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);
332 453
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));
334 456
335 using (IDataReader reader = dbConnection.ExecuteReader(query, UserData.Username))457 if (securityGroups.Length > 0)
336 {458 {
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"])));
338 }464 }
339 }465 }
340 466
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"]))
342 {474 {
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)
344 {479 {
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"]);
346 486
347 if (!UserData.Roles.Contains(roleName, StringComparer.InvariantCultureIgnoreCase))487 if (!UserData.Roles.Contains(roleName, StringComparer.OrdinalIgnoreCase))
348 UserData.Roles.Add(roleName);488 UserData.Roles.Add(roleName);
489 }
349 }490 }
350 }491 }
492 }
351 493
352 // Cache last user roles494 // Cache last user roles
353 ThreadPool.QueueUserWorkItem(CacheLastUserRole);495 ThreadPool.QueueUserWorkItem(CacheLastUserRoles);
354 496
355 return true;497 return true;
356 }
357 }498 }
358 catch (Exception ex)499 catch (Exception ex)
359 {500 {
@@ -410,7 +551,7 @@ namespace GSF.Security · 50 unmodified lines
410 {551 {
411 Password = password;552 Password = password;
412 553
413 // Authenticate against backend datastore554 // Authenticate against backend data store
414 UserData.IsAuthenticated =555 UserData.IsAuthenticated =
415 UserData.Password == password ||556 UserData.Password == password ||
416 UserData.Password == SecurityProviderUtility.EncryptPassword(password) ||557 UserData.Password == SecurityProviderUtility.EncryptPassword(password) ||
@@ -442,7 +583,7 @@ namespace GSF.Security · 25 unmodified lines
442 }583 }
443 }584 }
444 585
445 // If an exception occured during authentication, rethrow it after loging authentication attempt586 // If an exception occurred during authentication, rethrow it after logging authentication attempt
446 if ((object)authenticationException != null)587 if ((object)authenticationException != null)
447 {588 {
448 m_lastException = authenticationException;589 m_lastException = authenticationException;
@@ -454,7 +595,7 @@ namespace GSF.Security · 5 unmodified lines
454 }595 }
455 596
456 /// <summary>597 /// <summary>
457 /// Changes user password in the backend datastore.598 /// Changes user password in the backend data store.
458 /// </summary>599 /// </summary>
459 /// <param name="oldPassword">User's current password.</param>600 /// <param name="oldPassword">User's current password.</param>
460 /// <param name="newPassword">User's new password.</param>601 /// <param name="newPassword">User's new password.</param>
@@ -472,22 +613,23 @@ namespace GSF.Security · 11 unmodified lines
472 if (oldPassword == newPassword)613 if (oldPassword == newPassword)
473 throw new Exception("New password cannot be same as old password.");614 throw new Exception("New password cannot be same as old password.");
474 615
475 // Perform password change for internal users.616 // If needed, perform password change for internal NT/AD users.
476 if (!UserData.IsExternal)617 if (!UserData.IsExternal)
477 return base.ChangePassword(oldPassword, newPassword);618 return base.ChangePassword(oldPassword, newPassword);
478 619
479 // Verify old password.620 // Verify old password.
480 UserData.PasswordChangeDateTime = DateTime.MinValue;621 UserData.PasswordChangeDateTime = DateTime.MinValue;
622
481 if (!Authenticate(oldPassword))623 if (!Authenticate(oldPassword))
482 return false;624 return false;
483 625
484 // Verify new password.626 // Verify new password.
485 if (!Regex.IsMatch(newPassword, PasswordRequirementRegex))627 if (!Regex.IsMatch(newPassword, m_passwordRequirementsRegex))
486 throw new SecurityException(PasswordRequirementError);628 throw new SecurityException(m_passwordRequirementsError);
487 629
488 using (IDbConnection dbConnection = (new AdoDataConnection(SettingsCategory)).Connection)630 using (IDbConnection dbConnection = (new AdoDataConnection(SettingsCategory)).Connection)
489 {631 {
490 if (dbConnection == null)632 if ((object)dbConnection == null)
491 return false;633 return false;
492 634
493 bool oracle = dbConnection.GetType().Name == "OracleConnection";635 bool oracle = dbConnection.GetType().Name == "OracleConnection";
@@ -550,7 +692,7 @@ namespace GSF.Security · 56 unmodified lines
550 /// <returns>true if logging was successful, otherwise false.</returns>692 /// <returns>true if logging was successful, otherwise false.</returns>
551 protected virtual bool LogAuthenticationAttempt(bool loginSuccess)693 protected virtual bool LogAuthenticationAttempt(bool loginSuccess)
552 {694 {
553 if (UserData != null && !string.IsNullOrWhiteSpace(UserData.Username))695 if ((object)UserData != null && !string.IsNullOrWhiteSpace(UserData.Username))
554 {696 {
555 string message = string.Format("User \"{0}\" login attempt {1}.", UserData.Username, loginSuccess ? "succeeded using " + (m_successfulPassThroughAuthentication ? "pass-through authentication" : "user acquired password") : "failed");697 string message = string.Format("User \"{0}\" login attempt {1}.", UserData.Username, loginSuccess ? "succeeded using " + (m_successfulPassThroughAuthentication ? "pass-through authentication" : "user acquired password") : "failed");
556 EventLogEntryType entryType = loginSuccess ? EventLogEntryType.SuccessAudit : EventLogEntryType.FailureAudit;698 EventLogEntryType entryType = loginSuccess ? EventLogEntryType.SuccessAudit : EventLogEntryType.FailureAudit;
@@ -589,7 +731,7 @@ namespace GSF.Security · 32 unmodified lines
589 }731 }
590 732
591 /// <summary>733 /// <summary>
592 /// Logs information about an encountered exception to the backend datastore.734 /// Logs information about an encountered exception to the backend data store.
593 /// </summary>735 /// </summary>
594 /// <param name="source">Source of the exception.</param>736 /// <param name="source">Source of the exception.</param>
595 /// <param name="message">Detailed description of the exception.</param>737 /// <param name="message">Detailed description of the exception.</param>
@@ -631,50 +773,43 @@ namespace GSF.Security · 35 unmodified lines
631 return configSettings["LdapPath"].Value;773 return configSettings["LdapPath"].Value;
632 }774 }
633 775
634 // Cache last user role and monitor for changes776 // Cache last user roles and monitor for changes
635 private void CacheLastUserRole(object state)777 private void CacheLastUserRoles(object state)
636 {778 {
637 try779 try
638 {780 {
639 // Using an interprocess cache for user roles781 // Using an inter-process cache for user roles
640 UserRoleCache userRoleCache = UserRoleCache.GetCurrentCache();782 UserRoleCache userRoleCache = UserRoleCache.GetCurrentCache();
641 string cachedRole, currentRole;783 HashSet<string> currentRoles;
784 string[] cachedRoles;
642 785
643 // Attempt to retrieve cached user role786 // Retrieve current user roles
644 cachedRole = userRoleCache[UserData.Username];787 currentRoles = new HashSet<string>(UserData.Roles, StringComparer.OrdinalIgnoreCase);
645 788
646 // Determine current role with maximum rights for user789 // Attempt to retrieve cached user roles
647 string[] roles = UserData.Roles.ToArray();790 cachedRoles = userRoleCache[UserData.Username];
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";
656 791
657 bool roleChanged = false;792 bool rolesChanged = false;
658 string message;793 string message;
659 EventLogEntryType entryType;794 EventLogEntryType entryType;
660 795
661 if (string.IsNullOrEmpty(cachedRole))796 if ((object)cachedRoles == null || cachedRoles.Length == 0)
662 {797 {
663 // New user access granted798 // 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(", "));
665 entryType = EventLogEntryType.Information;800 entryType = EventLogEntryType.Information;
666 roleChanged = true;801 rolesChanged = true;
667 }802 }
668 else if (string.Compare(cachedRole, currentRole, true) != 0)803 else if (!currentRoles.SetEquals(cachedRoles))
669 {804 {
670 // User role access changed805 // 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(", "));
672 entryType = EventLogEntryType.Warning;807 entryType = EventLogEntryType.Warning;
673 roleChanged = true;808 rolesChanged = true;
674 }809 }
675 else810 else
676 {811 {
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(", "));
678 entryType = EventLogEntryType.SuccessAudit;813 entryType = EventLogEntryType.SuccessAudit;
679 }814 }
680 815
@@ -689,8 +824,8 @@ namespace GSF.Security · 8 unmodified lines
689 }824 }
690 825
691 // If role has changed, update cache826 // If role has changed, update cache
692 if (roleChanged)827 if (rolesChanged)
693 userRoleCache[UserData.Username] = currentRole;828 userRoleCache[UserData.Username] = currentRoles.ToArray();
694 }829 }
695 catch (Exception ex)830 catch (Exception ex)
696 {831 {
@@ -706,5 +841,75 @@ namespace GSF.Security · 9 unmodified lines
706 }841 }
707 842
708 #endregion843 #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
709 }914 }
710}915}
Source/Libraries/GSF.Security/GSF.Security.csproj+1−0
@@ -143,6 +143,7 @@ · 142 unmodified lines
143 </ItemGroup>143 </ItemGroup>
144 <ItemGroup>144 <ItemGroup>
145 <Compile Include="AdoSecurityProvider.cs" />145 <Compile Include="AdoSecurityProvider.cs" />
146 <Compile Include="AdoSecurityCache.cs" />
146 <Compile Include="UserRoleCache.cs" />147 <Compile Include="UserRoleCache.cs" />
147 <Compile Include="LdapSecurityProvider.cs" />148 <Compile Include="LdapSecurityProvider.cs" />
148 <Compile Include="RestrictAccessAttribute.cs" />149 <Compile Include="RestrictAccessAttribute.cs" />
Source/Libraries/GSF.Security/LdapSecurityProvider.cs+29−28
@@ -285,7 +285,7 @@ namespace GSF.Security · 284 unmodified lines
285 // Load settings from the specified category.285 // Load settings from the specified category.
286 ConfigurationFile config = ConfigurationFile.Current;286 ConfigurationFile config = ConfigurationFile.Current;
287 CategorizedSettingsElementCollection settings = config.Settings[SettingsCategory];287 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.");
289 settings.Add("CacheRetryDelayInterval", m_cacheRetryDelayInterval, "Wait interval, in milliseconds, before retrying load of user data cache.");289 settings.Add("CacheRetryDelayInterval", m_cacheRetryDelayInterval, "Wait interval, in milliseconds, before retrying load of user data cache.");
290 settings.Add("CacheMaximumRetryAttempts", m_cacheMaximumRetryAttempts, "Maximum retry attempts allowed for loading user data cache.");290 settings.Add("CacheMaximumRetryAttempts", m_cacheMaximumRetryAttempts, "Maximum retry attempts allowed for loading user data cache.");
291 EnableOfflineCaching = settings["EnableOfflineCaching"].ValueAs(m_enableOfflineCaching);291 EnableOfflineCaching = settings["EnableOfflineCaching"].ValueAs(m_enableOfflineCaching);
@@ -312,15 +312,19 @@ namespace GSF.Security · 20 unmodified lines
312 {312 {
313 // Validate with current thread principal.313 // Validate with current thread principal.
314 m_windowsPrincipal = Thread.CurrentPrincipal as WindowsPrincipal;314 m_windowsPrincipal = Thread.CurrentPrincipal as WindowsPrincipal;
315 UserData.IsAuthenticated = m_windowsPrincipal != null && !string.IsNullOrEmpty(UserData.LoginID) &&315
316 string.Compare(m_windowsPrincipal.Identity.Name, UserData.LoginID, true) == 0 && m_windowsPrincipal.Identity.IsAuthenticated;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;
317 }321 }
318 else322 else
319 {323 {
320 // Validate by performing network logon.324 // Validate by performing network logon.
321 string[] userParts = UserData.LoginID.Split('\\');325 string[] userParts = UserData.LoginID.Split('\\');
322 m_windowsPrincipal = UserInfo.AuthenticateUser(userParts[0], userParts[1], password) as WindowsPrincipal;326 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;
324 }328 }
325 329
326 return UserData.IsAuthenticated;330 return UserData.IsAuthenticated;
@@ -332,10 +336,10 @@ namespace GSF.Security · 5 unmodified lines
332 /// <returns>true if <see cref="SecurityProviderBase.UserData"/> is refreshed, otherwise false.</returns>336 /// <returns>true if <see cref="SecurityProviderBase.UserData"/> is refreshed, otherwise false.</returns>
333 public override bool RefreshData()337 public override bool RefreshData()
334 {338 {
335 bool result;
336
337 // For consistency with WindowIdentity principal, user groups are loaded into Roles collection339 // 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)
339 {343 {
340 string[] parts;344 string[] parts;
341 345
@@ -360,7 +364,7 @@ namespace GSF.Security · 18 unmodified lines
360 /// <returns>true if <see cref="SecurityProviderBase.UserData"/> is refreshed, otherwise false.</returns>364 /// <returns>true if <see cref="SecurityProviderBase.UserData"/> is refreshed, otherwise false.</returns>
361 protected virtual bool RefreshData(List<string> groupCollection, int providerID)365 protected virtual bool RefreshData(List<string> groupCollection, int providerID)
362 {366 {
363 if (groupCollection == null)367 if ((object)groupCollection == null)
364 throw new ArgumentNullException("groupCollection");368 throw new ArgumentNullException("groupCollection");
365 369
366 if (string.IsNullOrEmpty(UserData.Username))370 if (string.IsNullOrEmpty(UserData.Username))
@@ -393,6 +397,7 @@ namespace GSF.Security · 26 unmodified lines
393 397
394 // Create user info object using specified LDAP path if provided398 // Create user info object using specified LDAP path if provided
395 string ldapPath = GetLdapPath();399 string ldapPath = GetLdapPath();
400
396 if (string.IsNullOrEmpty(ldapPath))401 if (string.IsNullOrEmpty(ldapPath))
397 user = new UserInfo(UserData.Username);402 user = new UserInfo(UserData.Username);
398 else403 else
@@ -428,7 +433,7 @@ namespace GSF.Security · 29 unmodified lines
428 groupCollection.Add(groupName);433 groupCollection.Add(groupName);
429 }434 }
430 435
431 if (userDataCache != null)436 if ((object)userDataCache != null)
432 {437 {
433 // Cache user data so that information can be loaded later if domain is unavailable438 // Cache user data so that information can be loaded later if domain is unavailable
434 userDataCache[UserData.LoginID] = UserData;439 userDataCache[UserData.LoginID] = UserData;
@@ -440,8 +445,9 @@ namespace GSF.Security · 5 unmodified lines
440 else445 else
441 {446 {
442 // Attempt to load previously cached user information when domain is offline447 // Attempt to load previously cached user information when domain is offline
443 UserData cachedUserData = null;448 UserData cachedUserData;
444 if (userDataCache != null && userDataCache.TryGetUserData(UserData.LoginID, out cachedUserData))449
450 if ((object)userDataCache != null && userDataCache.TryGetUserData(UserData.LoginID, out cachedUserData))
445 {451 {
446 // Copy relevant cached user information452 // Copy relevant cached user information
447 UserData.FirstName = cachedUserData.FirstName;453 UserData.FirstName = cachedUserData.FirstName;
@@ -473,19 +479,19 @@ namespace GSF.Security · 25 unmodified lines
473 }479 }
474 finally480 finally
475 {481 {
476 if (user != null)482 if ((object)user != null)
477 {483 {
478 user.PersistSettings = false;484 user.PersistSettings = false;
479 user.Dispose();485 user.Dispose();
480 }486 }
481 487
482 if (userDataCache != null)488 if ((object)userDataCache != null)
483 userDataCache.Dispose();489 userDataCache.Dispose();
484 }490 }
485 }491 }
486 492
487 /// <summary>493 /// <summary>
488 /// Changes user password in the backend datastore.494 /// Changes user password in the backend data store.
489 /// </summary>495 /// </summary>
490 /// <param name="oldPassword">User's current password.</param>496 /// <param name="oldPassword">User's current password.</param>
491 /// <param name="newPassword">User's new password.</param>497 /// <param name="newPassword">User's new password.</param>
@@ -534,17 +540,17 @@ namespace GSF.Security · 42 unmodified lines
534 catch (TargetInvocationException ex)540 catch (TargetInvocationException ex)
535 {541 {
536 // Propagate password change error542 // Propagate password change error
537 if (ex.InnerException == null)543 if ((object)ex.InnerException == null)
538 throw new SecurityException(ex.Message, ex);544 throw new SecurityException(ex.Message, ex);
539 else545 else
540 throw new SecurityException(ex.InnerException.Message, ex);546 throw new SecurityException(ex.InnerException.Message, ex);
541 }547 }
542 finally548 finally
543 {549 {
544 if (user != null)550 if ((object)user != null)
545 user.Dispose();551 user.Dispose();
546 552
547 if (context != null)553 if ((object)context != null)
548 UserInfo.EndImpersonation(context);554 UserInfo.EndImpersonation(context);
549 555
550 RefreshData();556 RefreshData();
@@ -572,19 +578,14 @@ namespace GSF.Security · 21 unmodified lines
572 /// <returns>The LDAP path.</returns>578 /// <returns>The LDAP path.</returns>
573 protected virtual string GetLdapPath()579 protected virtual string GetLdapPath()
574 {580 {
575 if (ConnectionString.StartsWith("LDAP://", StringComparison.InvariantCultureIgnoreCase) ||581 if (ConnectionString.StartsWith("LDAP://", StringComparison.InvariantCultureIgnoreCase) || ConnectionString.StartsWith("LDAPS://", StringComparison.InvariantCultureIgnoreCase))
576 ConnectionString.StartsWith("LDAPS://", StringComparison.InvariantCultureIgnoreCase))
577 {
578 return ConnectionString;582 return ConnectionString;
579 }583
580 else584 foreach (KeyValuePair<string, string> pair in ConnectionString.ParseKeyValuePairs())
581 {585 {
582 foreach (KeyValuePair<string, string> pair in ConnectionString.ParseKeyValuePairs())586 if (pair.Value.StartsWith("LDAP://", StringComparison.InvariantCultureIgnoreCase) ||
583 {587 pair.Value.StartsWith("LDAPS://", StringComparison.InvariantCultureIgnoreCase))
584 if (pair.Value.StartsWith("LDAP://", StringComparison.InvariantCultureIgnoreCase) ||588 return pair.Value;
585 pair.Value.StartsWith("LDAPS://", StringComparison.InvariantCultureIgnoreCase))
586 return pair.Value;
587 }
588 }589 }
589 590
590 return null;591 return null;
Source/Libraries/GSF.Security/SecurityProviderBase.cs+3−3
@@ -284,7 +284,7 @@ namespace GSF.Security · 283 unmodified lines
284 }284 }
285 285
286 /// <summary>286 /// <summary>
287 /// Gets or sets <see cref="SecurePassword"/> as cleartext password.287 /// Gets or sets <see cref="SecurePassword"/> as clear text password.
288 /// </summary>288 /// </summary>
289 public unsafe string Password289 public unsafe string Password
290 {290 {
@@ -314,7 +314,7 @@ namespace GSF.Security · 23 unmodified lines
314 {314 {
315 SecureString securePassword = new SecureString();315 SecureString securePassword = new SecureString();
316 316
317 if (value == null)317 if ((object)value == null)
318 value = string.Empty;318 value = string.Empty;
319 319
320 foreach (char c in value)320 foreach (char c in value)
@@ -621,7 +621,7 @@ namespace GSF.Security · 300 unmodified lines
621 {621 {
622 m_disposed = true; // Prevent duplicate dispose.622 m_disposed = true; // Prevent duplicate dispose.
623 623
624 if (Disposed != null)624 if ((object)Disposed != null)
625 Disposed(this, EventArgs.Empty);625 Disposed(this, EventArgs.Empty);
626 }626 }
627 }627 }
Source/Libraries/GSF.Security/UserData.cs+99−23
@@ -25,7 +25,7 @@ · 24 unmodified lines
25// 01/14/2011 - Pinal C. Patel25// 01/14/2011 - Pinal C. Patel
26// Updated field attributes used by XmlSerializer.26// Updated field attributes used by XmlSerializer.
27// 12/20/2012 - Starlynn Danyelle Gilliam27// 12/20/2012 - Starlynn Danyelle Gilliam
28// Modifeid Header.28// Modified Header.
29//29//
30//******************************************************************************************************30//******************************************************************************************************
31 31
@@ -37,7 +37,7 @@ using System.Xml.Serialization; · 5 unmodified lines
37namespace GSF.Security37namespace GSF.Security
38{38{
39 /// <summary>39 /// <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.
41 /// </summary>41 /// </summary>
42 [XmlType(Namespace = ""), DataContract(Namespace = ""), Serializable]42 [XmlType(Namespace = ""), DataContract(Namespace = ""), Serializable]
43 public class UserData43 public class UserData
@@ -73,115 +73,191 @@ namespace GSF.Security · 29 unmodified lines
73 /// <summary>73 /// <summary>
74 /// Gets the user's login ID.74 /// Gets the user's login ID.
75 /// </summary>75 /// </summary>
76 public string LoginID { get; set; }76 public string LoginID
77 {
78 get;
79 set;
80 }
77 81
78 /// <summary>82 /// <summary>
79 /// Gets the user's login name.83 /// Gets the user's login name.
80 /// </summary>84 /// </summary>
81 [XmlElement, DataMember(Order = 0)]85 [XmlElement, DataMember(Order = 0)]
82 public string Username { get; set; }86 public string Username
87 {
88 get;
89 set;
90 }
83 91
84 /// <summary>92 /// <summary>
85 /// Gets the user's password.93 /// Gets the user's password.
86 /// </summary>94 /// </summary>
87 [XmlElement, DataMember(Order = 1)]95 [XmlElement, DataMember(Order = 1)]
88 public string Password { get; set; }96 public string Password
97 {
98 get;
99 set;
100 }
89 101
90 /// <summary>102 /// <summary>
91 /// Gets the user's first name.103 /// Gets the user's first name.
92 /// </summary>104 /// </summary>
93 [XmlElement, DataMember(Order = 2)]105 [XmlElement, DataMember(Order = 2)]
94 public string FirstName { get; set; }106 public string FirstName
107 {
108 get;
109 set;
110 }
95 111
96 /// <summary>112 /// <summary>
97 /// Gets the user's last name.113 /// Gets the user's last name.
98 /// </summary>114 /// </summary>
99 [XmlElement, DataMember(Order = 3)]115 [XmlElement, DataMember(Order = 3)]
100 public string LastName { get; set; }116 public string LastName
117 {
118 get;
119 set;
120 }
101 121
102 /// <summary>122 /// <summary>
103 /// Gets the user's company name.123 /// Gets the user's company name.
104 /// </summary>124 /// </summary>
105 [XmlElement, DataMember(Order = 4)]125 [XmlElement, DataMember(Order = 4)]
106 public string CompanyName { get; set; }126 public string CompanyName
127 {
128 get;
129 set;
130 }
107 131
108 /// <summary>132 /// <summary>
109 /// Gets the user's phone number.133 /// Gets the user's phone number.
110 /// </summary>134 /// </summary>
111 [XmlElement, DataMember(Order = 5)]135 [XmlElement, DataMember(Order = 5)]
112 public string PhoneNumber { get; set; }136 public string PhoneNumber
137 {
138 get;
139 set;
140 }
113 141
114 /// <summary>142 /// <summary>
115 /// Gets the user's email address.143 /// Gets the user's email address.
116 /// </summary>144 /// </summary>
117 [XmlElement, DataMember(Order = 6)]145 [XmlElement, DataMember(Order = 6)]
118 public string EmailAddress { get; set; }146 public string EmailAddress
147 {
148 get;
149 set;
150 }
119 151
120 /// <summary>152 /// <summary>
121 /// Gets the user's security question.153 /// Gets the user's security question.
122 /// </summary>154 /// </summary>
123 [XmlElement, DataMember(Order = 7)]155 [XmlElement, DataMember(Order = 7)]
124 public string SecurityQuestion { get; set; }156 public string SecurityQuestion
157 {
158 get;
159 set;
160 }
125 161
126 /// <summary>162 /// <summary>
127 /// Gets the user's security answer.163 /// Gets the user's security answer.
128 /// </summary>164 /// </summary>
129 [XmlElement, DataMember(Order = 8)]165 [XmlElement, DataMember(Order = 8)]
130 public string SecurityAnswer { get; set; }166 public string SecurityAnswer
167 {
168 get;
169 set;
170 }
131 171
132 /// <summary>172 /// <summary>
133 /// Gets the UTC date and time when user must change the password.173 /// Gets the UTC date and time when user must change the password.
134 /// </summary>174 /// </summary>
135 [XmlElement, DataMember(Order = 9)]175 [XmlElement, DataMember(Order = 9)]
136 public DateTime PasswordChangeDateTime { get; set; }176 public DateTime PasswordChangeDateTime
177 {
178 get;
179 set;
180 }
137 181
138 /// <summary>182 /// <summary>
139 /// Gets the UTC date and time when user account was created.183 /// Gets the UTC date and time when user account was created.
140 /// </summary>184 /// </summary>
141 [XmlElement, DataMember(Order = 10)]185 [XmlElement, DataMember(Order = 10)]
142 public DateTime AccountCreatedDateTime { get; set; }186 public DateTime AccountCreatedDateTime
187 {
188 get;
189 set;
190 }
143 191
144 /// <summary>192 /// <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.
146 /// </summary>194 /// </summary>
147 [XmlElement, DataMember(Order = 11)]195 [XmlElement, DataMember(Order = 11)]
148 public bool IsDefined { get; set; }196 public bool IsDefined
197 {
198 get;
199 set;
200 }
149 201
150 /// <summary>202 /// <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.
152 /// </summary>204 /// </summary>
153 [XmlElement, DataMember(Order = 12)]205 [XmlElement, DataMember(Order = 12)]
154 public bool IsExternal { get; set; }206 public bool IsExternal
207 {
208 get;
209 set;
210 }
155 211
156 /// <summary>212 /// <summary>
157 /// Gets a boolean value that indicates whether the user account has been disabled.213 /// Gets a boolean value that indicates whether the user account has been disabled.
158 /// </summary>214 /// </summary>
159 [XmlElement, DataMember(Order = 13)]215 [XmlElement, DataMember(Order = 13)]
160 public bool IsDisabled { get; set; }216 public bool IsDisabled
217 {
218 get;
219 set;
220 }
161 221
162 /// <summary>222 /// <summary>
163 /// Gets a boolean value that indicates whether the user account has been locked due to numerous unsuccessful login attempts.223 /// Gets a boolean value that indicates whether the user account has been locked due to numerous unsuccessful login attempts.
164 /// </summary>224 /// </summary>
165 [XmlElement, DataMember(Order = 14)]225 [XmlElement, DataMember(Order = 14)]
166 public bool IsLockedOut { get; set; }226 public bool IsLockedOut
227 {
228 get;
229 set;
230 }
167 231
168 /// <summary>232 /// <summary>
169 /// Gets a boolean value indicating whether or not the user has been authenticated.233 /// Gets a boolean value indicating whether or not the user has been authenticated.
170 /// </summary>234 /// </summary>
171 [XmlElement, DataMember(Order = 15)]235 [XmlElement, DataMember(Order = 15)]
172 public bool IsAuthenticated { get; set; }236 public bool IsAuthenticated
237 {
238 get;
239 set;
240 }
173 241
174 /// <summary>242 /// <summary>
175 /// Gets a read-only list of all the groups the user belongs to.243 /// Gets a read-only list of all the groups the user belongs to.
176 /// </summary>244 /// </summary>
177 [XmlArray, XmlArrayItem("Group"), DataMember(Order = 16)]245 [XmlArray, XmlArrayItem("Group"), DataMember(Order = 16)]
178 public List<string> Groups { get; set; }246 public List<string> Groups
247 {
248 get;
249 set;
250 }
179 251
180 /// <summary>252 /// <summary>
181 /// Gets a read-only list of all the roles assigned to the user.253 /// Gets a read-only list of all the roles assigned to the user.
182 /// </summary>254 /// </summary>
183 [XmlArray, XmlArrayItem("Role"), DataMember(Order = 17)]255 [XmlArray, XmlArrayItem("Role"), DataMember(Order = 17)]
184 public List<string> Roles { get; set; }256 public List<string> Roles
257 {
258 get;
259 set;
260 }
185 261
186 #endregion262 #endregion
187 263
Source/Libraries/GSF.Security/UserDataCache.cs+17−7
@@ -24,7 +24,7 @@ · 23 unmodified lines
24// Renamed RetryDelayInterval and MaximumRetryAttempts settings persisted to the config file 24// Renamed RetryDelayInterval and MaximumRetryAttempts settings persisted to the config file
25// to CacheRetryDelayInterval and CacheMaximumRetryAttempts for clarity.25// to CacheRetryDelayInterval and CacheMaximumRetryAttempts for clarity.
26// 08/12/2011 - J. Ritchie Carroll26// 08/12/2011 - J. Ritchie Carroll
27// Modifed static GetCurrentCache to accept settings category of host security provider27// Modified static GetCurrentCache to accept settings category of host security provider
28// implementation in case the category has been changed from the default value by the consumer.28// implementation in case the category has been changed from the default value by the consumer.
29// 08/16/2011 - Pinal C. Patel29// 08/16/2011 - Pinal C. Patel
30// Modified GetCurrentCache() to just set the FileName property and not the RetryDelayInterval, 30// Modified GetCurrentCache() to just set the FileName property and not the RetryDelayInterval,
@@ -46,8 +46,19 @@ using GSF.Threading; · 15 unmodified lines
46namespace GSF.Security46namespace GSF.Security
47{47{
48 /// <summary>48 /// <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"/>.
50 /// </summary>50 /// </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>
51 public class UserDataCache : InterprocessCache62 public class UserDataCache : InterprocessCache
52 {63 {
53 #region [ Members ]64 #region [ Members ]
@@ -59,7 +70,7 @@ namespace GSF.Security · 5 unmodified lines
59 70
60 // Fields71 // Fields
61 private Dictionary<string, UserData> m_userDataTable; // Internal dictionary of serialized user data72 private Dictionary<string, UserData> m_userDataTable; // Internal dictionary of serialized user data
62 private readonly object m_userDataTableLock; // Lock object73 private readonly object m_userDataTableLock; // Lock object for internal dictionary
63 private int m_providerID; // Unique provider ID used to distinguish cached user data that may be different based on provider74 private int m_providerID; // Unique provider ID used to distinguish cached user data that may be different based on provider
64 75
65 #endregion76 #endregion
@@ -108,7 +119,7 @@ namespace GSF.Security · 42 unmodified lines
108 }119 }
109 120
110 /// <summary>121 /// <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.
112 /// </summary>123 /// </summary>
113 public int ProviderID124 public int ProviderID
114 {125 {
@@ -183,7 +194,7 @@ namespace GSF.Security · 68 unmodified lines
183 }194 }
184 195
185 /// <summary>196 /// <summary>
186 /// Initiates interprocess synchronized save of user data cache.197 /// Initiates inter-process synchronized save of user data cache.
187 /// </summary>198 /// </summary>
188 public override void Save()199 public override void Save()
189 {200 {
@@ -195,7 +206,7 @@ namespace GSF.Security · 5 unmodified lines
195 serializedUserDataTable = Serialization.Serialize(m_userDataTable, SerializationFormat.Binary);206 serializedUserDataTable = Serialization.Serialize(m_userDataTable, SerializationFormat.Binary);
196 }207 }
197 208
198 // File data is the serialized user data table, assigmnent will initiate auto-save if needed209 // File data is the serialized user data table, assignment will initiate auto-save if needed
199 FileData = serializedUserDataTable;210 FileData = serializedUserDataTable;
200 }211 }
201 212
@@ -296,6 +307,5 @@ namespace GSF.Security · 94 unmodified lines
296 }307 }
297 308
298 #endregion309 #endregion
299
300 }310 }
301}311}
Source/Libraries/GSF.Security/UserRoleCache.cs+51−44
@@ -21,20 +21,27 @@ · 20 unmodified lines
21//21//
22//******************************************************************************************************22//******************************************************************************************************
23 23
24using GSF.Collections;
25using GSF.IO;
26using GSF.Security.Cryptography;
27using GSF.Threading;
28using System;24using System;
29using System.Collections.Generic;25using System.Collections.Generic;
30using System.IO;26using System.IO;
31using System.Security.Cryptography;27using System.Security.Cryptography;
28using GSF.Collections;
29using GSF.IO;
30using GSF.Security.Cryptography;
31using GSF.Threading;
32 32
33namespace GSF.Security33namespace GSF.Security
34{34{
35 /// <summary>35 /// <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.
37 /// </summary>37 /// </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>
38 public class UserRoleCache : InterprocessCache45 public class UserRoleCache : InterprocessCache
39 {46 {
40 #region [ Members ]47 #region [ Members ]
@@ -45,8 +52,8 @@ namespace GSF.Security · 4 unmodified lines
45 private const string DefaultCacheFileName = "UserRoleCache.bin";52 private const string DefaultCacheFileName = "UserRoleCache.bin";
46 53
47 // Fields54 // Fields
48 private Dictionary<string, string> m_userRoles; // Internal dictionary of serialized user roles55 private Dictionary<string, string[]> m_userRoles; // Internal dictionary of serialized user roles
49 private readonly object m_userRolesLock; // Lock object56 private readonly object m_userRolesLock; // Lock object for internal dictionary
50 57
51 #endregion58 #endregion
52 59
@@ -57,7 +64,7 @@ namespace GSF.Security · 4 unmodified lines
57 public UserRoleCache(int maximumConcurrentLocks = InterprocessReaderWriterLock.DefaultMaximumConcurrentLocks)64 public UserRoleCache(int maximumConcurrentLocks = InterprocessReaderWriterLock.DefaultMaximumConcurrentLocks)
58 : base(maximumConcurrentLocks)65 : base(maximumConcurrentLocks)
59 {66 {
60 m_userRoles = new Dictionary<string, string>();67 m_userRoles = new Dictionary<string, string[]>();
61 m_userRolesLock = new object();68 m_userRolesLock = new object();
62 }69 }
63 70
@@ -66,11 +73,11 @@ namespace GSF.Security · 2 unmodified lines
66 /// <summary>73 /// <summary>
67 /// Gets a copy of the internal user role dictionary.74 /// Gets a copy of the internal user role dictionary.
68 /// </summary>75 /// </summary>
69 public Dictionary<string, string> UserRoles76 public Dictionary<string, string[]> UserRoles
70 {77 {
71 get78 get
72 {79 {
73 Dictionary<string, string> userRoles;80 Dictionary<string, string[]> userRoles;
74 81
75 // We wait until the user roles cache is loaded before attempting to access it82 // We wait until the user roles cache is loaded before attempting to access it
76 WaitForDataReady();83 WaitForDataReady();
@@ -78,8 +85,8 @@ namespace GSF.Security · 1 unmodified line
78 // Wait for thread level lock on dictionary85 // Wait for thread level lock on dictionary
79 lock (m_userRolesLock)86 lock (m_userRolesLock)
80 {87 {
81 // Make a copy of the user role table for external use88 // Make a copy of the user role dictionary for external use
82 userRoles = new Dictionary<string, string>(m_userRoles);89 userRoles = new Dictionary<string, string[]>(m_userRoles);
83 }90 }
84 91
85 return userRoles;92 return userRoles;
@@ -87,17 +94,17 @@ namespace GSF.Security · 1 unmodified line
87 }94 }
88 95
89 /// <summary>96 /// <summary>
90 /// Gets or sets access role for given <paramref name="userName"/>.97 /// Gets or sets access roles for given <paramref name="userName"/>.
91 /// </summary>98 /// </summary>
92 /// <param name="userName">User name for associated access role to load or save.</param>99 /// <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>100 /// <returns>Access roles for given <paramref name="userName"/> if found; otherwise <c>null</c>.</returns>
94 public string this[string userName]101 public string[] this[string userName]
95 {102 {
96 get103 get
97 {104 {
98 string role;105 string[] roles;
99 TryGetUserRole(userName, out role);106 TryGetUserRole(userName, out roles);
100 return role;107 return roles;
101 }108 }
102 set109 set
103 {110 {
@@ -113,9 +120,9 @@ namespace GSF.Security · 9 unmodified lines
113 /// Attempts to retrieve access role for given <paramref name="userName"/>.120 /// Attempts to retrieve access role for given <paramref name="userName"/>.
114 /// </summary>121 /// </summary>
115 /// <param name="userName">User name associated with access role to retrieve.</param>122 /// <param name="userName">User name associated with access role to retrieve.</param>
116 /// <param name="role">Access role to populate if found.</param>123 /// <param name="roles">Access roles 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>124 /// <returns><c>true</c> if access roles for given <paramref name="userName"/> were retrieved; otherwise <c>false</c>.</returns>
118 public bool TryGetUserRole(string userName, out string role)125 public bool TryGetUserRole(string userName, out string[] roles)
119 {126 {
120 string hash = HashLoginID(userName);127 string hash = HashLoginID(userName);
121 bool result;128 bool result;
@@ -123,42 +130,42 @@ namespace GSF.Security · 1 unmodified line
123 // We wait until the cache is loaded before attempting to access it130 // We wait until the cache is loaded before attempting to access it
124 WaitForDataReady();131 WaitForDataReady();
125 132
126 // Wait for thread level lock on user info table133 // Wait for thread level lock on user info dictionary
127 lock (m_userRolesLock)134 lock (m_userRolesLock)
128 {135 {
129 // Attempt to lookup persisted access role based on hash of user name136 // 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);
131 }138 }
132 139
133 return result;140 return result;
134 }141 }
135 142
136 /// <summary>143 /// <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"/>.
138 /// </summary>145 /// </summary>
139 /// <param name="userName">User name associated with access role to retrieve.</param>146 /// <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>
141 /// <remarks>148 /// <remarks>
142 /// <para>149 /// <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;
144 /// otherwise existing entry will be updated.151 /// otherwise existing entry will be updated.
145 /// </para>152 /// </para>
146 /// <para>153 /// <para>
147 /// Updates are automatically queued up for serialization so user does not need to call <see cref="Save"/>.154 /// Updates are automatically queued up for serialization so user does not need to call <see cref="Save"/>.
148 /// </para>155 /// </para>
149 /// </remarks>156 /// </remarks>
150 public void SaveUserRole(string userName, string role)157 public void SaveUserRole(string userName, string[] roles)
151 {158 {
152 string hash = HashLoginID(userName);159 string hash = HashLoginID(userName);
153 160
154 // We wait until the cache is loaded before attempting to access it161 // We wait until the cache is loaded before attempting to access it
155 WaitForDataReady();162 WaitForDataReady();
156 163
157 // Wait for thread level lock on user data table164 // Wait for thread level lock on user data dictionary
158 lock (m_userRolesLock)165 lock (m_userRolesLock)
159 {166 {
160 // Assign new user information to user data table167 // Assign new user information to user data dictionary
161 m_userRoles[hash] = role;168 m_userRoles[hash] = roles;
162 }169 }
163 170
164 // Queue up a serialization for this new user information171 // Queue up a serialization for this new user information
@@ -172,7 +179,7 @@ namespace GSF.Security · 7 unmodified lines
172 public void MergeLeft(UserRoleCache other)179 public void MergeLeft(UserRoleCache other)
173 {180 {
174 // Merge other roles into local ones181 // Merge other roles into local ones
175 Dictionary<string, string> mergedUserRoles = UserRoles.Merge(other.UserRoles);182 Dictionary<string, string[]> mergedUserRoles = UserRoles.Merge(other.UserRoles);
176 183
177 // Wait for thread level lock on dictionary184 // Wait for thread level lock on dictionary
178 lock (m_userRolesLock)185 lock (m_userRolesLock)
@@ -192,7 +199,7 @@ namespace GSF.Security · 13 unmodified lines
192 public void MergeRight(UserRoleCache other)199 public void MergeRight(UserRoleCache other)
193 {200 {
194 // Merge other roles into local ones201 // Merge other roles into local ones
195 Dictionary<string, string> mergedUserRoles = other.UserRoles.Merge(UserRoles);202 Dictionary<string, string[]> mergedUserRoles = other.UserRoles.Merge(UserRoles);
196 203
197 // Wait for thread level lock on dictionary204 // Wait for thread level lock on dictionary
198 lock (m_userRolesLock)205 lock (m_userRolesLock)
@@ -206,7 +213,7 @@ namespace GSF.Security · 7 unmodified lines
206 }213 }
207 214
208 /// <summary>215 /// <summary>
209 /// Initiates interprocess synchronized save of user role cache.216 /// Initiates inter-process synchronized save of user role cache.
210 /// </summary>217 /// </summary>
211 public override void Save()218 public override void Save()
212 {219 {
@@ -218,7 +225,7 @@ namespace GSF.Security · 5 unmodified lines
218 serializedUserDataTable = Serialization.Serialize(m_userRoles, SerializationFormat.Binary);225 serializedUserDataTable = Serialization.Serialize(m_userRoles, SerializationFormat.Binary);
219 }226 }
220 227
221 // File data is the serialized user roles dictionary, assigmnent will initiate auto-save if needed228 // File data is the serialized user roles dictionary, assignment will initiate auto-save if needed
222 FileData = serializedUserDataTable;229 FileData = serializedUserDataTable;
223 }230 }
224 231
@@ -248,7 +255,7 @@ namespace GSF.Security · 23 unmodified lines
248 {255 {
249 // Decrypt data that was encrypted local to this machine256 // Decrypt data that was encrypted local to this machine
250 byte[] serializedUserRoles = ProtectedData.Unprotect(fileStream.ReadStream(), null, DataProtectionScope.LocalMachine);257 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);
252 259
253 // Wait for thread level lock on user role dictionary260 // Wait for thread level lock on user role dictionary
254 lock (m_userRolesLock)261 lock (m_userRolesLock)
@@ -266,7 +273,7 @@ namespace GSF.Security · 11 unmodified lines
266 /// <param name="userName">User name to hash.</param>273 /// <param name="userName">User name to hash.</param>
267 /// <returns>The Base64 encoded calculated SHA-2 hash of the <paramref name="userName"/> used as the key for the user roles dictionary.</returns>274 /// <returns>The Base64 encoded calculated SHA-2 hash of the <paramref name="userName"/> used as the key for the user roles dictionary.</returns>
268 /// <remarks>275 /// <remarks>
269 /// For added security, a hash of the <paramref name="userName"/> is used as the key for access roles276 /// For added security, a hash of the <paramref name="userName"/> is used as the key for accessing roles
270 /// in the user roles cache instead of the actual <paramref name="userName"/>. This method allows the277 /// in the user roles cache instead of the actual <paramref name="userName"/>. This method allows the
271 /// consumer to properly calculate this hash when directly using the user data cache.278 /// consumer to properly calculate this hash when directly using the user data cache.
272 /// </remarks>279 /// </remarks>
@@ -324,8 +331,8 @@ namespace GSF.Security · 51 unmodified lines
324 331
325 try332 try
326 {333 {
327 // Validate that user has write access to the local cryptographic cache folder334 // Validate that user has write access to the local cache folder
328 string tempFile = FilePath.GetDirectoryName(localCacheFileName) + Guid.NewGuid().ToString() + ".tmp";335 string tempFile = FilePath.GetDirectoryName(localCacheFileName) + Guid.NewGuid() + ".tmp";
329 336
330 using (File.Create(tempFile))337 using (File.Create(tempFile))
331 {338 {
@@ -334,13 +341,13 @@ namespace GSF.Security · 2 unmodified lines
334 if (File.Exists(tempFile))341 if (File.Exists(tempFile))
335 File.Delete(tempFile);342 File.Delete(tempFile);
336 343
337 // No access issues exist, use local cache as the primary cryptographic key and initialization vector cache344 // No access issues exist, use local cache as the primary cache
338 currentCache = localUserRoleCache;345 currentCache = localUserRoleCache;
339 currentCache.AutoSave = true;346 currentCache.AutoSave = true;
340 }347 }
341 catch (UnauthorizedAccessException)348 catch (UnauthorizedAccessException)
342 {349 {
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,
344 // use a path where user will have rights351 // use a path where user will have rights
345 string userCacheFolder = FilePath.AddPathSuffix(FilePath.GetApplicationDataFolder());352 string userCacheFolder = FilePath.AddPathSuffix(FilePath.GetApplicationDataFolder());
346 string userCacheFileName = userCacheFolder + FilePath.GetFileName(localCacheFileName);353 string userCacheFileName = userCacheFolder + FilePath.GetFileName(localCacheFileName);
@@ -349,11 +356,11 @@ namespace GSF.Security · 2 unmodified lines
349 if (!Directory.Exists(userCacheFolder))356 if (!Directory.Exists(userCacheFolder))
350 Directory.CreateDirectory(userCacheFolder);357 Directory.CreateDirectory(userCacheFolder);
351 358
352 // Copy existing common cryptographic cache if none exists359 // Copy existing common cache if none exists
353 if (File.Exists(localCacheFileName) && !File.Exists(userCacheFileName))360 if (File.Exists(localCacheFileName) && !File.Exists(userCacheFileName))
354 File.Copy(localCacheFileName, userCacheFileName);361 File.Copy(localCacheFileName, userCacheFileName);
355 362
356 // Initialize primary cryptographic key and initialization vector cache within user folder363 // Initialize primary cache within user folder
357 currentCache = new UserRoleCache364 currentCache = new UserRoleCache
358 {365 {
359 FileName = userCacheFileName,366 FileName = userCacheFileName,
@@ -367,10 +374,10 @@ namespace GSF.Security · 7 unmodified lines
367 AutoSave = true374 AutoSave = true
368 };375 };
369 376
370 // Load initial keys377 // Load initial roles
371 currentCache.Load();378 currentCache.Load();
372 379
373 // Merge new or updated keys, protected folder keys taking precendence over user keys380 // Merge new or updated roles, protected folder roles taking precedence over user folder roles
374 currentCache.MergeRight(localUserRoleCache);381 currentCache.MergeRight(localUserRoleCache);
375 }382 }
376 383
Source/Libraries/GSF.TimeSeries/ServiceHostBase.cs+12−0
@@ -52,6 +52,7 @@ using GSF.Data; · 51 unmodified lines
52using GSF.IO;52using GSF.IO;
53using GSF.Net.Security;53using GSF.Net.Security;
54using GSF.Reflection;54using GSF.Reflection;
55using GSF.Security;
55using GSF.ServiceProcess;56using GSF.ServiceProcess;
56using GSF.TimeSeries.Adapters;57using GSF.TimeSeries.Adapters;
57using GSF.Units;58using GSF.Units;
@@ -968,6 +969,17 @@ namespace GSF.TimeSeries · 910 unmodified lines
968 DisplayStatusMessage("{0} configuration pre-cache completed in {1}.", UpdateType.Information, source.TableName, operationElapsedTime < 0.01D ? "less than a second" : operationElapsedTime.ToString("0.00") + " seconds");969 DisplayStatusMessage("{0} configuration pre-cache completed in {1}.", UpdateType.Information, source.TableName, operationElapsedTime < 0.01D ? "less than a second" : operationElapsedTime.ToString("0.00") + " seconds");
969 }970 }
970 971
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
971 DisplayStatusMessage("Database configuration successfully loaded.", UpdateType.Information);983 DisplayStatusMessage("Database configuration successfully loaded.", UpdateType.Information);
972 }984 }
973 catch (Exception ex)985 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