Unified · Split
| @@ -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) |
| @@ -37,11 +37,11 @@ using Timer = System.Timers.Timer; · 36 unmodified lines | |||
| 37 | namespace GSF.IO | 37 | namespace 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 for | 43 | /// 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 : IDisposable | 46 | 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 | // Fields | 64 | // Fields |
| 65 | private string m_fileName; // Path and file name of file needing interprocess synchronization | 65 | private string m_fileName; // Path and file name of file needing inter-process synchronization |
| 66 | private byte[] m_fileData; // Data loaded or to be saved | 66 | private byte[] m_fileData; // Data loaded or to be saved |
| 67 | private bool m_autoSave; // Flag to auto save when file data has changed | 67 | private bool m_autoSave; // Flag to auto save when file data has changed |
| 68 | private InterprocessReaderWriterLock m_fileLock; // Interprocess reader/writer lock used to synchronize file access | 68 | private InterprocessReaderWriterLock m_fileLock; // Inter-process reader/writer lock used to synchronize file access |
| 69 | private ReaderWriterLockSlim m_dataLock; // Thread level reader/writer lock used to synchronize file data access | 69 | 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 load | 70 | 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 save | 71 | 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 changes | 72 | private FileSystemWatcher m_fileWatcher; // Optional file watcher used to reload changes |
| 73 | private readonly int m_maximumConcurrentLocks; // Maximum concurrent reader locks allowed | 73 | private readonly int m_maximumConcurrentLocks; // Maximum concurrent reader locks allowed |
| 74 | private int m_maximumRetryAttempts; // Maximum retry attempts allowed for loading file | 74 | private int m_maximumRetryAttempts; // Maximum retry attempts allowed for loading file |
| 75 | private readonly BitArray m_retryQueue; // Retry event queue | 75 | private readonly BitArray m_retryQueue; // Retry event queue |
| 76 | private Timer m_retryTimer; // File I/O retry timer | 76 | private Timer m_retryTimer; // File I/O retry timer |
| 77 | private long m_lastRetryTime; // Time of last retry attempt | 77 | private long m_lastRetryTime; // Time of last retry attempt |
| 78 | private int m_retryCount; // Total number of retries attempted so far | 78 | private int m_retryCount; // Total number of retries attempted so far |
| 79 | private bool m_disposed; // Class disposed flag | 79 | 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 FileName | 130 | public string FileName |
| 131 | { | 131 | { |
| 132 | get | 132 | get |
| 133 | { | 133 | { |
| @@ -136,7 +136,7 @@ namespace GSF.IO · 2 unmodified lines | |||
| 136 | set | 136 | 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[] FileData | 157 | public byte[] FileData |
| 158 | { | 158 | { |
| 159 | get | 159 | get |
| 160 | { | 160 | { |
| @@ -179,7 +179,7 @@ namespace GSF.IO · 18 unmodified lines | |||
| 179 | set | 179 | 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 AutoSave | 216 | public bool AutoSave |
| 217 | { | 217 | { |
| 218 | get | 218 | 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 ReloadOnChange | 231 | public bool ReloadOnChange |
| 232 | { | 232 | { |
| 233 | get | 233 | 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 updates | 244 | // 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 MaximumConcurrentLocks | 264 | public int MaximumConcurrentLocks |
| 265 | { | 265 | { |
| 266 | get | 266 | 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 MaximumRetryAttempts | 275 | public int MaximumRetryAttempts |
| 276 | { | 276 | { |
| 277 | get | 277 | 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 RetryDelayInterval | 290 | public double RetryDelayInterval |
| 291 | { | 291 | { |
| 292 | get | 292 | 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 | try | 472 | 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 | try | 485 | 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 | try | 489 | // Disable file watch notification before update |
| 487 | { | 490 | if ((object)m_fileWatcher != null) |
| 488 | // Disable file watch notification before update | 491 | 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 | else | 496 | 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 | else | 510 | 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 | try | 543 | 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 | try | 558 | 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 | try | 562 | 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 | else | 564 | 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 | else | 577 | 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 | else | 589 | else |
| 594 | { | 590 | { |
| 595 | // File doesn't exist, create ane empty array representing a zero-length file | 591 | 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 unhandled | 624 | // 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 load | 625 | // 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 forever | 631 | // 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 counter | 634 | } |
| 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 process | 643 | // 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 event | 662 | 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> |
| @@ -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 | try | 473 | try |
| 474 | { | 474 | { |
| 475 | // Validate that user has write access to the local cryptographic cache folder | 475 | // Validate that user has write access to the local cryptographic cache folder |
| 476 | #if MONO | 476 | 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 cache | 485 | // No access issues exist, use local cache as the primary cryptographic key and initialization vector cache |
| 490 | s_keyIVCache = localKeyIVCache; | 486 | s_keyIVCache = localKeyIVCache; |
| @@ -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 parameter | 88 | // 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 active | 92 | // 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 rights | 213 | // 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 | #endif | 217 | #endif |
| @@ -29,7 +29,7 @@ using System.Threading; · 28 unmodified lines | |||
| 29 | namespace GSF.Threading | 29 | namespace 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 : IDisposable | 34 | 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 | // Fields | 45 | // Fields |
| 46 | private Mutex m_semaphoreLock; // Mutex used to synchronize access to Semaphore | 46 | private Mutex m_semaphoreLock; // Mutex used to synchronize access to Semaphore |
| 47 | private Semaphore m_concurrencyLock; // Semaphore used for reader/writer lock on consumer object | 47 | 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 | #endregion | 51 | #endregion |
| @@ -56,7 +56,7 @@ namespace GSF.Threading · 4 unmodified lines | |||
| 56 | /// Creates a new instance of the <see cref="InterprocessReaderWriterLock"/> associated with the specified | 56 | /// 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 specified | 66 | /// 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 simply | 72 | /// 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 | finally | 143 | 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 | try | 213 | 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 | } |
| @@ -0,0 +1,296 @@ | |||
| 1 | //****************************************************************************************************** | ||
| 2 | // AdoSecurityCache.cs - Gbtc | ||
| 3 | // | ||
| 4 | // Copyright © 2013, Grid Protection Alliance. All Rights Reserved. | ||
| 5 | // | ||
| 6 | // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | ||
| 7 | // the NOTICE file distributed with this work for additional information regarding copyright ownership. | ||
| 8 | // The GPA licenses this file to you under the Eclipse Public License -v 1.0 (the "License"); you may | ||
| 9 | // not use this file except in compliance with the License. You may obtain a copy of the License at: | ||
| 10 | // | ||
| 11 | // http://www.opensource.org/licenses/eclipse-1.0.php | ||
| 12 | // | ||
| 13 | // Unless agreed to in writing, the subject software distributed under the License is distributed on an | ||
| 14 | // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | ||
| 15 | // License for the specific language governing permissions and limitations. | ||
| 16 | // | ||
| 17 | // Code Modification History: | ||
| 18 | // ---------------------------------------------------------------------------------------------------- | ||
| 19 | // 12/30/2013 - J. Ritchie Carroll | ||
| 20 | // Generated original version of source code. | ||
| 21 | // | ||
| 22 | //****************************************************************************************************** | ||
| 23 | |||
| 24 | using System; | ||
| 25 | using System.Data; | ||
| 26 | using System.IO; | ||
| 27 | using System.Security.Cryptography; | ||
| 28 | using GSF.Configuration; | ||
| 29 | using GSF.Data; | ||
| 30 | using GSF.IO; | ||
| 31 | using GSF.Threading; | ||
| 32 | |||
| 33 | namespace GSF.Security | ||
| 34 | { | ||
| 35 | /// <summary> | ||
| 36 | /// Represents a secured inter-process cache for the security context needed by the <see cref="AdoSecurityProvider"/>. | ||
| 37 | /// </summary> | ||
| 38 | /// <remarks> | ||
| 39 | /// This is a system cache that contains the last known security information loaded from the database related to users, | ||
| 40 | /// roles and groups. This cache allows the <see cref="AdoSecurityProvider"/> to start-up without database access | ||
| 41 | /// using latest cached security context. Even though this cache contains role information for all users, it does | ||
| 42 | /// not overlap information in the <see cref="UserRoleCache"/> since the user role cache contains only the highest | ||
| 43 | /// order role assignment for a user at last login and is used for auditing. | ||
| 44 | /// </remarks> | ||
| 45 | public class AdoSecurityCache : InterprocessCache | ||
| 46 | { | ||
| 47 | #region [ Members ] | ||
| 48 | |||
| 49 | // Constants | ||
| 50 | |||
| 51 | // Default ADO security cache file name | ||
| 52 | private const string DefaultCacheFileName = "AdoSecurityCache.bin"; | ||
| 53 | |||
| 54 | // Fields | ||
| 55 | private DataSet m_dataSet; // Internal ADO security data set | ||
| 56 | private readonly object m_dataSetLock; // Lock object for internal data set | ||
| 57 | |||
| 58 | #endregion | ||
| 59 | |||
| 60 | /// <summary> | ||
| 61 | /// Creates a new instance of the <see cref="AdoSecurityCache"/> with the specified number of <paramref name="maximumConcurrentLocks"/>. | ||
| 62 | /// </summary> | ||
| 63 | /// <param name="maximumConcurrentLocks">Maximum concurrent reader locks to allow.</param> | ||
| 64 | public AdoSecurityCache(int maximumConcurrentLocks = InterprocessReaderWriterLock.DefaultMaximumConcurrentLocks) | ||
| 65 | : base(maximumConcurrentLocks) | ||
| 66 | { | ||
| 67 | m_dataSet = new DataSet("AdoSecurityContext"); | ||
| 68 | m_dataSetLock = new object(); | ||
| 69 | } | ||
| 70 | |||
| 71 | #region [ Properties ] | ||
| 72 | |||
| 73 | /// <summary> | ||
| 74 | /// Gets or sets the internal <see cref="DataSet"/>; returned value will be a copy of the internal. | ||
| 75 | /// </summary> | ||
| 76 | public DataSet DataSet | ||
| 77 | { | ||
| 78 | get | ||
| 79 | { | ||
| 80 | DataSet dataSet; | ||
| 81 | |||
| 82 | // We wait until the data set cache is loaded before attempting to access it | ||
| 83 | WaitForDataReady(); | ||
| 84 | |||
| 85 | // Wait for thread level lock on data set | ||
| 86 | lock (m_dataSetLock) | ||
| 87 | { | ||
| 88 | // Since user could changes, make a copy of the data set for external use | ||
| 89 | dataSet = m_dataSet.Copy(); | ||
| 90 | } | ||
| 91 | |||
| 92 | return dataSet; | ||
| 93 | } | ||
| 94 | set | ||
| 95 | { | ||
| 96 | // Wait for thread level lock on data set | ||
| 97 | lock (m_dataSetLock) | ||
| 98 | { | ||
| 99 | // Assign new data set | ||
| 100 | m_dataSet = value; | ||
| 101 | } | ||
| 102 | |||
| 103 | // Queue up a serialization for this new data set | ||
| 104 | Save(); | ||
| 105 | } | ||
| 106 | } | ||
| 107 | |||
| 108 | #endregion | ||
| 109 | |||
| 110 | #region [ Methods ] | ||
| 111 | |||
| 112 | /// <summary> | ||
| 113 | /// Initiates inter-process synchronized save of <see cref="DataSet"/>. | ||
| 114 | /// </summary> | ||
| 115 | public override void Save() | ||
| 116 | { | ||
| 117 | byte[] serializedDataSet; | ||
| 118 | |||
| 119 | // Wait for thread level lock on data set | ||
| 120 | lock (m_dataSetLock) | ||
| 121 | { | ||
| 122 | using (BlockAllocatedMemoryStream stream = new BlockAllocatedMemoryStream()) | ||
| 123 | { | ||
| 124 | m_dataSet.SerializeToStream(stream); | ||
| 125 | serializedDataSet = stream.ToArray(); | ||
| 126 | } | ||
| 127 | } | ||
| 128 | |||
| 129 | // File data is the serialized data set, assignment will initiate auto-save if needed | ||
| 130 | FileData = serializedDataSet; | ||
| 131 | } | ||
| 132 | |||
| 133 | /// <summary> | ||
| 134 | /// Handles serialization of file to disk; virtual method allows customization (e.g., pre-save encryption and/or data merge). | ||
| 135 | /// </summary> | ||
| 136 | /// <param name="fileStream"><see cref="FileStream"/> used to serialize data.</param> | ||
| 137 | /// <param name="fileData">File data to be serialized.</param> | ||
| 138 | /// <remarks> | ||
| 139 | /// Consumers overriding this method should not directly call <see cref="InterprocessCache.FileData"/> property to avoid potential dead-locks. | ||
| 140 | /// </remarks> | ||
| 141 | protected override void SaveFileData(FileStream fileStream, byte[] fileData) | ||
| 142 | { | ||
| 143 | // Encrypt data local to this machine (this way user cannot copy ADO security cache to another machine) | ||
| 144 | base.SaveFileData(fileStream, ProtectedData.Protect(fileData, null, DataProtectionScope.LocalMachine)); | ||
| 145 | } | ||
| 146 | |||
| 147 | /// <summary> | ||
| 148 | /// Handles deserialization of file from disk; virtual method allows customization (e.g., pre-load decryption and/or data merge). | ||
| 149 | /// </summary> | ||
| 150 | /// <param name="fileStream"><see cref="FileStream"/> used to deserialize data.</param> | ||
| 151 | /// <returns>Deserialized file data.</returns> | ||
| 152 | /// <remarks> | ||
| 153 | /// Consumers overriding this method should not directly call <see cref="InterprocessCache.FileData"/> property to avoid potential dead-locks. | ||
| 154 | /// </remarks> | ||
| 155 | protected override byte[] LoadFileData(FileStream fileStream) | ||
| 156 | { | ||
| 157 | // Decrypt data that was encrypted local to this machine | ||
| 158 | byte[] serializedDataSet = ProtectedData.Unprotect(fileStream.ReadStream(), null, DataProtectionScope.LocalMachine); | ||
| 159 | DataSet dataSet; | ||
| 160 | |||
| 161 | using (MemoryStream stream = new MemoryStream(serializedDataSet)) | ||
| 162 | { | ||
| 163 | dataSet = stream.DeserializeToDataSet(); | ||
| 164 | } | ||
| 165 | |||
| 166 | // Wait for thread level lock on data set | ||
| 167 | lock (m_dataSetLock) | ||
| 168 | { | ||
| 169 | m_dataSet = dataSet; | ||
| 170 | } | ||
| 171 | |||
| 172 | return serializedDataSet; | ||
| 173 | } | ||
| 174 | |||
| 175 | // Waits until the cache is loaded before attempting to access it | ||
| 176 | private void WaitForDataReady() | ||
| 177 | { | ||
| 178 | try | ||
| 179 | { | ||
| 180 | // Just wrapping this method to provide a more detailed exception message if there is an issue loading cache | ||
| 181 | WaitForLoad(); | ||
| 182 | } | ||
| 183 | catch (Exception ex) | ||
| 184 | { | ||
| 185 | throw new UnauthorizedAccessException("ADO security cache access failure: timeout while attempting to load last valid security context.", ex); | ||
| 186 | } | ||
| 187 | } | ||
| 188 | |||
| 189 | #endregion | ||
| 190 | |||
| 191 | #region [ Static ] | ||
| 192 | |||
| 193 | // Static Methods | ||
| 194 | |||
| 195 | /// <summary> | ||
| 196 | /// Loads the <see cref="AdoSecurityCache"/> for the current local user. | ||
| 197 | /// </summary> | ||
| 198 | /// <returns>Loaded instance of the <see cref="AdoSecurityCache"/>.</returns> | ||
| 199 | public static AdoSecurityCache GetCurrentCache() | ||
| 200 | { | ||
| 201 | AdoSecurityCache currentCache; | ||
| 202 | AdoSecurityCache localSecurityCache; | ||
| 203 | |||
| 204 | // Define default cache path | ||
| 205 | string cachePath = string.Format("{0}\\ConfigurationCache\\", FilePath.GetAbsolutePath("")); | ||
| 206 | |||
| 207 | // Make sure configuration cache path setting exists | ||
| 208 | ConfigurationFile configFile = ConfigurationFile.Current; | ||
| 209 | CategorizedSettingsElementCollection systemSettings = configFile.Settings["systemSettings"]; | ||
| 210 | systemSettings.Add("ConfigurationCachePath", cachePath, "Defines the path used to cache serialized configurations"); | ||
| 211 | |||
| 212 | // Retrieve configuration cache path as defined in the config file | ||
| 213 | cachePath = systemSettings["ConfigurationCachePath"].Value; | ||
| 214 | |||
| 215 | // Make sure configuration cache path exists | ||
| 216 | if (!Directory.Exists(cachePath)) | ||
| 217 | Directory.CreateDirectory(cachePath); | ||
| 218 | |||
| 219 | string localCacheFileName = Path.Combine(cachePath, DefaultCacheFileName); | ||
| 220 | |||
| 221 | // Initialize local ADO security cache (application may only have read-only access to this cache) | ||
| 222 | localSecurityCache = new AdoSecurityCache | ||
| 223 | { | ||
| 224 | FileName = localCacheFileName, | ||
| 225 | #if DNF45 | ||
| 226 | ReloadOnChange = true, | ||
| 227 | #else | ||
| 228 | // Reload on change is disabled to eliminate GC handle leaks on .NET 4.0, this prevents | ||
| 229 | // automatic runtime reloading of key/iv data cached by another application. | ||
| 230 | ReloadOnChange = false, | ||
| 231 | #endif | ||
| 232 | AutoSave = false | ||
| 233 | }; | ||
| 234 | |||
| 235 | // Load initial ADO security data set | ||
| 236 | localSecurityCache.Load(); | ||
| 237 | |||
| 238 | try | ||
| 239 | { | ||
| 240 | // Validate that current user has write access to the local cache folder | ||
| 241 | string tempFile = FilePath.GetDirectoryName(localCacheFileName) + Guid.NewGuid() + ".tmp"; | ||
| 242 | |||
| 243 | using (File.Create(tempFile)) | ||
| 244 | { | ||
| 245 | } | ||
| 246 | |||
| 247 | if (File.Exists(tempFile)) | ||
| 248 | File.Delete(tempFile); | ||
| 249 | |||
| 250 | // No access issues exist, use local cache as the primary cache | ||
| 251 | currentCache = localSecurityCache; | ||
| 252 | currentCache.AutoSave = true; | ||
| 253 | } | ||
| 254 | catch (UnauthorizedAccessException) | ||
| 255 | { | ||
| 256 | // User does not have needed serialization access to common cache folder, | ||
| 257 | // use a path where user will have rights | ||
| 258 | string userCacheFolder = FilePath.AddPathSuffix(FilePath.GetApplicationDataFolder()); | ||
| 259 | string userCacheFileName = userCacheFolder + FilePath.GetFileName(localCacheFileName); | ||
| 260 | |||
| 261 | // Make sure user directory exists | ||
| 262 | if (!Directory.Exists(userCacheFolder)) | ||
| 263 | Directory.CreateDirectory(userCacheFolder); | ||
| 264 | |||
| 265 | // Copy existing common cache if none exists | ||
| 266 | if (File.Exists(localCacheFileName) && !File.Exists(userCacheFileName)) | ||
| 267 | File.Copy(localCacheFileName, userCacheFileName); | ||
| 268 | |||
| 269 | // Initialize primary cache within user folder | ||
| 270 | currentCache = new AdoSecurityCache | ||
| 271 | { | ||
| 272 | FileName = userCacheFileName, | ||
| 273 | #if DNF45 | ||
| 274 | ReloadOnChange = true, | ||
| 275 | #else | ||
| 276 | // Reload on change is disabled to eliminate GC handle leaks on .NET 4.0, this prevents | ||
| 277 | // automatic runtime reloading of key/iv data cached by another application. | ||
| 278 | ReloadOnChange = false, | ||
| 279 | #endif | ||
| 280 | AutoSave = true | ||
| 281 | }; | ||
| 282 | |||
| 283 | // Load initial ADO security data set | ||
| 284 | currentCache.Load(); | ||
| 285 | |||
| 286 | // Update user located security cache if locally located security cache is newer | ||
| 287 | if (File.GetLastWriteTime(localCacheFileName) > File.GetLastWriteTime(userCacheFileName)) | ||
| 288 | currentCache.DataSet = localSecurityCache.DataSet; | ||
| 289 | } | ||
| 290 | |||
| 291 | return currentCache; | ||
| 292 | } | ||
| 293 | |||
| 294 | #endregion | ||
| 295 | } | ||
| 296 | } | ||
| @@ -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 Gilliam | 28 | // 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 | ||
| 33 | using System; | 36 | using System; |
| 37 | using System.Collections.Generic; | ||
| 34 | using System.Configuration; | 38 | using System.Configuration; |
| 35 | using System.Data; | 39 | using System.Data; |
| 36 | using System.Diagnostics; | 40 | using System.Diagnostics; |
| @@ -39,17 +43,19 @@ using System.Security; · 2 unmodified lines | |||
| 39 | using System.Text.RegularExpressions; | 43 | using System.Text.RegularExpressions; |
| 40 | using System.Threading; | 44 | using System.Threading; |
| 41 | using System.Web.Security; | 45 | using System.Web.Security; |
| 46 | using GSF.Collections; | ||
| 42 | using GSF.Configuration; | 47 | using GSF.Configuration; |
| 43 | using GSF.Data; | 48 | using GSF.Data; |
| 49 | using GSF.Identity; | ||
| 44 | 50 | ||
| 45 | namespace GSF.Security | 51 | namespace 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 its | 54 | /// 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 : LdapSecurityProvider | 154 | public class AdoSecurityProvider : LdapSecurityProvider |
| 92 | { | 155 | { |
| 93 | #region [ Members ] | 156 | #region [ Members ] |
| 94 | 157 | ||
| 95 | // Constants | 158 | // 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 | #endregion | 186 | #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 | #endregion | 216 | #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 CanUpdateData | 223 | public override bool CanUpdateData |
| 143 | { | 224 | { |
| 144 | get | 225 | 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 | else | 231 | // 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 exist | 263 | // 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 database | 361 | 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 groups | 364 | 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 roles | 393 | // At this point an NT/AD based user will have a list of groups that is known to be available to the user. A database |
| 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 roles | 406 | // 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 roles | 494 | // 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 datastore | 554 | // 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 attempt | 586 | // 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 changes | 776 | // Cache last user roles and monitor for changes |
| 635 | private void CacheLastUserRole(object state) | 777 | private void CacheLastUserRoles(object state) |
| 636 | { | 778 | { |
| 637 | try | 779 | try |
| 638 | { | 780 | { |
| 639 | // Using an interprocess cache for user roles | 781 | // 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 role | 786 | // 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 user | 789 | // 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 granted | 798 | // 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 changed | 805 | // 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 | else | 810 | 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 cache | 826 | // 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 | #endregion | 843 | #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 | } |
| @@ -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" /> |
| @@ -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 | else | 322 | 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 collection | 339 | // 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 provided | 398 | // 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 | else | 403 | 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 unavailable | 438 | // 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 | else | 445 | else |
| 441 | { | 446 | { |
| 442 | // Attempt to load previously cached user information when domain is offline | 447 | // 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 information | 452 | // 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 | finally | 480 | 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 error | 542 | // 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 | else | 545 | else |
| 540 | throw new SecurityException(ex.InnerException.Message, ex); | 546 | throw new SecurityException(ex.InnerException.Message, ex); |
| 541 | } | 547 | } |
| 542 | finally | 548 | 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 | else | 584 | 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; |
| @@ -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 Password | 289 | 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 | } |
| @@ -25,7 +25,7 @@ · 24 unmodified lines | |||
| 25 | // 01/14/2011 - Pinal C. Patel | 25 | // 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 Gilliam | 27 | // 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 | |||
| 37 | namespace GSF.Security | 37 | namespace 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 UserData | 43 | 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 | #endregion | 262 | #endregion |
| 187 | 263 | ||
| @@ -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 Carroll | 26 | // 08/12/2011 - J. Ritchie Carroll |
| 27 | // Modifed static GetCurrentCache to accept settings category of host security provider | 27 | // Modified static GetCurrentCache to accept settings category of host security provider |
| 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. Patel | 29 | // 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 | |||
| 46 | namespace GSF.Security | 46 | namespace 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 : InterprocessCache | 62 | 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 | // Fields | 71 | // Fields |
| 61 | private Dictionary<string, UserData> m_userDataTable; // Internal dictionary of serialized user data | 72 | private Dictionary<string, UserData> m_userDataTable; // Internal dictionary of serialized user data |
| 62 | private readonly object m_userDataTableLock; // Lock object | 73 | private readonly object m_userDataTableLock; // Lock object for internal dictionary |
| 63 | private int m_providerID; // Unique provider ID used to distinguish cached user data that may be different based on provider | 74 | private int m_providerID; // Unique provider ID used to distinguish cached user data that may be different based on provider |
| 64 | 75 | ||
| 65 | #endregion | 76 | #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 ProviderID | 124 | 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 needed | 209 | // 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 | #endregion | 309 | #endregion |
| 299 | |||
| 300 | } | 310 | } |
| 301 | } | 311 | } |
| @@ -21,20 +21,27 @@ · 20 unmodified lines | |||
| 21 | // | 21 | // |
| 22 | //****************************************************************************************************** | 22 | //****************************************************************************************************** |
| 23 | 23 | ||
| 24 | using GSF.Collections; | ||
| 25 | using GSF.IO; | ||
| 26 | using GSF.Security.Cryptography; | ||
| 27 | using GSF.Threading; | ||
| 28 | using System; | 24 | using System; |
| 29 | using System.Collections.Generic; | 25 | using System.Collections.Generic; |
| 30 | using System.IO; | 26 | using System.IO; |
| 31 | using System.Security.Cryptography; | 27 | using System.Security.Cryptography; |
| 28 | using GSF.Collections; | ||
| 29 | using GSF.IO; | ||
| 30 | using GSF.Security.Cryptography; | ||
| 31 | using GSF.Threading; | ||
| 32 | 32 | ||
| 33 | namespace GSF.Security | 33 | namespace 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 : InterprocessCache | 45 | 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 | // Fields | 54 | // Fields |
| 48 | private Dictionary<string, string> m_userRoles; // Internal dictionary of serialized user roles | 55 | private Dictionary<string, string[]> m_userRoles; // Internal dictionary of serialized user roles |
| 49 | private readonly object m_userRolesLock; // Lock object | 56 | private readonly object m_userRolesLock; // Lock object for internal dictionary |
| 50 | 57 | ||
| 51 | #endregion | 58 | #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> UserRoles | 76 | public Dictionary<string, string[]> UserRoles |
| 70 | { | 77 | { |
| 71 | get | 78 | 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 it | 82 | // 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 dictionary | 85 | // 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 use | 88 | // 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 | get | 103 | 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 | set | 109 | 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 it | 130 | // 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 table | 133 | // 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 name | 136 | // 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 it | 161 | // 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 table | 164 | // 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 table | 167 | // 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 information | 171 | // 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 ones | 181 | // 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 dictionary | 184 | // 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 ones | 201 | // 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 dictionary | 204 | // 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 needed | 228 | // 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 machine | 256 | // 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 dictionary | 260 | // 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 roles | 276 | /// 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 the | 277 | /// 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 | try | 332 | try |
| 326 | { | 333 | { |
| 327 | // Validate that user has write access to the local cryptographic cache folder | 334 | // 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 cache | 344 | // 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 rights | 351 | // 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 exists | 359 | // 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 folder | 363 | // Initialize primary cache within user folder |
| 357 | currentCache = new UserRoleCache | 364 | currentCache = new UserRoleCache |
| 358 | { | 365 | { |
| 359 | FileName = userCacheFileName, | 366 | FileName = userCacheFileName, |
| @@ -367,10 +374,10 @@ namespace GSF.Security · 7 unmodified lines | |||
| 367 | AutoSave = true | 374 | AutoSave = true |
| 368 | }; | 375 | }; |
| 369 | 376 | ||
| 370 | // Load initial keys | 377 | // Load initial roles |
| 371 | currentCache.Load(); | 378 | currentCache.Load(); |
| 372 | 379 | ||
| 373 | // Merge new or updated keys, protected folder keys taking precendence over user keys | 380 | // Merge new or updated roles, protected folder roles taking precedence over user folder roles |
| 374 | currentCache.MergeRight(localUserRoleCache); | 381 | currentCache.MergeRight(localUserRoleCache); |
| 375 | } | 382 | } |
| 376 | 383 | ||
| @@ -52,6 +52,7 @@ using GSF.Data; · 51 unmodified lines | |||
| 52 | using GSF.IO; | 52 | using GSF.IO; |
| 53 | using GSF.Net.Security; | 53 | using GSF.Net.Security; |
| 54 | using GSF.Reflection; | 54 | using GSF.Reflection; |
| 55 | using GSF.Security; | ||
| 55 | using GSF.ServiceProcess; | 56 | using GSF.ServiceProcess; |
| 56 | using GSF.TimeSeries.Adapters; | 57 | using GSF.TimeSeries.Adapters; |
| 57 | using GSF.Units; | 58 | using 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) |