Unified · Split
| @@ -124,6 +124,8 @@ namespace GSF.Data · 123 unmodified lines | ||
| 124 | 124 | if ((object)obj == null) |
| 125 | 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 | 129 | foreach (DataTable table in obj.Tables) |
| 128 | 130 | { |
| 129 | 131 | foreach (DataRow row in table.Rows) |
| @@ -37,11 +37,11 @@ using Timer = System.Timers.Timer; · 36 unmodified lines | ||
| 37 | 37 | namespace GSF.IO |
| 38 | 38 | { |
| 39 | 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 | 41 | /// </summary> |
| 42 | 42 | /// <remarks> |
| 43 | 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 | 45 | /// </remarks> |
| 46 | 46 | public class InterprocessCache : IDisposable |
| 47 | 47 | { |
| @@ -62,18 +62,18 @@ namespace GSF.IO · 14 unmodified lines | ||
| 62 | 62 | public const double DefaultRetryDelayInterval = 200.0D; |
| 63 | 63 | |
| 64 | 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 | 66 | private byte[] m_fileData; // Data loaded or to be saved |
| 67 | 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 | 69 | private ReaderWriterLockSlim m_dataLock; // Thread level reader/writer lock used to synchronize file data access |
| 70 | 70 | private ManualResetEventSlim m_loadIsReady; // Wait handle used so that system will wait for file data load |
| 71 | 71 | private ManualResetEventSlim m_saveIsReady; // Wait handle used so that system will wait for file data save |
| 72 | 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 | 74 | private int m_maximumRetryAttempts; // Maximum retry attempts allowed for loading file |
| 75 | − private readonly BitArray m_retryQueue; // Retry event queue | |
| 76 | − private Timer m_retryTimer; // File I/O retry timer | |
| 75 | + private readonly BitArray m_retryQueue; // Retry event queue | |
| 76 | + private Timer m_retryTimer; // File I/O retry timer | |
| 77 | 77 | private long m_lastRetryTime; // Time of last retry attempt |
| 78 | 78 | private int m_retryCount; // Total number of retries attempted so far |
| 79 | 79 | private bool m_disposed; // Class disposed flag |
| @@ -125,9 +125,9 @@ namespace GSF.IO · 45 unmodified lines | ||
| 125 | 125 | #region [ Properties ] |
| 126 | 126 | |
| 127 | 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 | 129 | /// </summary> |
| 130 | − public virtual string FileName | |
| 130 | + public string FileName | |
| 131 | 131 | { |
| 132 | 132 | get |
| 133 | 133 | { |
| @@ -136,7 +136,7 @@ namespace GSF.IO · 2 unmodified lines | ||
| 136 | 136 | set |
| 137 | 137 | { |
| 138 | 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 | 141 | m_fileName = FilePath.GetAbsolutePath(value); |
| 142 | 142 | |
| @@ -154,7 +154,7 @@ namespace GSF.IO · 11 unmodified lines | ||
| 154 | 154 | /// <remarks> |
| 155 | 155 | /// Setting value to <c>null</c> will create a zero-length file. |
| 156 | 156 | /// </remarks> |
| 157 | − public virtual byte[] FileData | |
| 157 | + public byte[] FileData | |
| 158 | 158 | { |
| 159 | 159 | get |
| 160 | 160 | { |
| @@ -179,7 +179,7 @@ namespace GSF.IO · 18 unmodified lines | ||
| 179 | 179 | set |
| 180 | 180 | { |
| 181 | 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 | 184 | bool dataChanged = false; |
| 185 | 185 | |
| @@ -213,7 +213,7 @@ namespace GSF.IO · 27 unmodified lines | ||
| 213 | 213 | /// <summary> |
| 214 | 214 | /// Gets or sets flag that determines if <see cref="InterprocessCache"/> should automatically initiate a save when <see cref="FileData"/> has been updated. |
| 215 | 215 | /// </summary> |
| 216 | − public virtual bool AutoSave | |
| 216 | + public bool AutoSave | |
| 217 | 217 | { |
| 218 | 218 | get |
| 219 | 219 | { |
| @@ -228,7 +228,7 @@ namespace GSF.IO · 8 unmodified lines | ||
| 228 | 228 | /// <summary> |
| 229 | 229 | /// Gets or sets flag that enables system to monitor for changes in <see cref="FileName"/> and automatically reload <see cref="FileData"/>. |
| 230 | 230 | /// </summary> |
| 231 | − public virtual bool ReloadOnChange | |
| 231 | + public bool ReloadOnChange | |
| 232 | 232 | { |
| 233 | 233 | get |
| 234 | 234 | { |
| @@ -239,7 +239,7 @@ namespace GSF.IO · 4 unmodified lines | ||
| 239 | 239 | if (value && (object)m_fileWatcher == null) |
| 240 | 240 | { |
| 241 | 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 | 244 | // Setup file watcher to monitor for external updates |
| 245 | 245 | m_fileWatcher = new FileSystemWatcher(); |
| @@ -261,7 +261,7 @@ namespace GSF.IO · 15 unmodified lines | ||
| 261 | 261 | /// <summary> |
| 262 | 262 | /// Gets the maximum concurrent reader locks allowed. |
| 263 | 263 | /// </summary> |
| 264 | − public virtual int MaximumConcurrentLocks | |
| 264 | + public int MaximumConcurrentLocks | |
| 265 | 265 | { |
| 266 | 266 | get |
| 267 | 267 | { |
| @@ -272,7 +272,7 @@ namespace GSF.IO · 4 unmodified lines | ||
| 272 | 272 | /// <summary> |
| 273 | 273 | /// Maximum retry attempts allowed for loading or saving cache file data. |
| 274 | 274 | /// </summary> |
| 275 | − public virtual int MaximumRetryAttempts | |
| 275 | + public int MaximumRetryAttempts | |
| 276 | 276 | { |
| 277 | 277 | get |
| 278 | 278 | { |
| @@ -287,7 +287,7 @@ namespace GSF.IO · 8 unmodified lines | ||
| 287 | 287 | /// <summary> |
| 288 | 288 | /// Wait interval, in milliseconds, before retrying load or save of cache file data. |
| 289 | 289 | /// </summary> |
| 290 | − public virtual double RetryDelayInterval | |
| 290 | + public double RetryDelayInterval | |
| 291 | 291 | { |
| 292 | 292 | get |
| 293 | 293 | { |
| @@ -368,7 +368,7 @@ namespace GSF.IO · 74 unmodified lines | ||
| 368 | 368 | } |
| 369 | 369 | |
| 370 | 370 | /// <summary> |
| 371 | − /// Initiates interprocess synchronized cache file save. | |
| 371 | + /// Initiates inter-process synchronized cache file save. | |
| 372 | 372 | /// </summary> |
| 373 | 373 | /// <remarks> |
| 374 | 374 | /// Subclasses should always call <see cref="WaitForLoad()"/> before calling this method. |
| @@ -376,17 +376,17 @@ namespace GSF.IO · 1 unmodified line | ||
| 376 | 376 | public virtual void Save() |
| 377 | 377 | { |
| 378 | 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 | 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 | 384 | m_saveIsReady.Reset(); |
| 385 | 385 | ThreadPool.QueueUserWorkItem(SynchronizedWrite); |
| 386 | 386 | } |
| 387 | 387 | |
| 388 | 388 | /// <summary> |
| 389 | − /// Initiates interprocess synchronized cache file load. | |
| 389 | + /// Initiates inter-process synchronized cache file load. | |
| 390 | 390 | /// </summary> |
| 391 | 391 | /// <remarks> |
| 392 | 392 | /// Subclasses should always call <see cref="WaitForLoad()"/> before calling this method. |
| @@ -394,7 +394,7 @@ namespace GSF.IO · 1 unmodified line | ||
| 394 | 394 | public virtual void Load() |
| 395 | 395 | { |
| 396 | 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 | 399 | m_loadIsReady.Reset(); |
| 400 | 400 | ThreadPool.QueueUserWorkItem(SynchronizedRead); |
| @@ -471,58 +471,58 @@ namespace GSF.IO · 70 unmodified lines | ||
| 471 | 471 | { |
| 472 | 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); | |
| 483 | − | |
| 484 | − if (m_dataLock.TryEnterReadLock((int)m_retryTimer.Interval)) | |
| 487 | + try | |
| 485 | 488 | { |
| 486 | − try | |
| 487 | − { | |
| 488 | − // Disable file watch notification before update | |
| 489 | − if ((object)m_fileWatcher != null) | |
| 490 | − m_fileWatcher.EnableRaisingEvents = false; | |
| 489 | + // Disable file watch notification before update | |
| 490 | + if ((object)m_fileWatcher != null) | |
| 491 | + m_fileWatcher.EnableRaisingEvents = false; | |
| 491 | 492 | |
| 492 | − SaveFileData(fileStream, m_fileData); | |
| 493 | − m_saveIsReady.Set(); | |
| 494 | − } | |
| 495 | − finally | |
| 496 | − { | |
| 497 | − m_dataLock.ExitReadLock(); | |
| 498 | − | |
| 499 | − // Reenable file watch notification | |
| 500 | − if ((object)m_fileWatcher != null) | |
| 501 | − m_fileWatcher.EnableRaisingEvents = true; | |
| 502 | − } | |
| 493 | + SaveFileData(fileStream, m_fileData); | |
| 494 | + m_saveIsReady.Set(); | |
| 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) | |
| 510 | − { | |
| 511 | − RetrySynchronizedEvent(ex, WriteEvent); | |
| 512 | − } | |
| 513 | − finally | |
| 505 | + else | |
| 514 | 506 | { |
| 515 | − m_fileLock.ExitWriteLock(); | |
| 516 | − | |
| 517 | − if ((object)fileStream != null) | |
| 518 | − fileStream.Close(); | |
| 507 | + RetrySynchronizedEvent(new TimeoutException("Timeout waiting to acquire read lock for local cache"), WriteEvent); | |
| 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 | 527 | catch (ThreadAbortException) |
| 528 | 528 | { |
| @@ -542,63 +542,63 @@ namespace GSF.IO · 13 unmodified lines | ||
| 542 | 542 | { |
| 543 | 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); | |
| 556 | − | |
| 557 | − if (m_dataLock.TryEnterWriteLock((int)m_retryTimer.Interval)) | |
| 560 | + try | |
| 558 | 561 | { |
| 559 | − try | |
| 560 | − { | |
| 561 | − m_fileData = LoadFileData(fileStream); | |
| 562 | − } | |
| 563 | − finally | |
| 564 | − { | |
| 565 | − m_dataLock.ExitWriteLock(); | |
| 566 | − } | |
| 567 | − | |
| 568 | − // Release any threads waiting for file data | |
| 569 | − m_loadIsReady.Set(); | |
| 562 | + m_fileData = LoadFileData(fileStream); | |
| 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) | |
| 577 | − { | |
| 578 | − RetrySynchronizedEvent(ex, ReadEvent); | |
| 579 | − } | |
| 580 | − finally | |
| 572 | + else | |
| 581 | 573 | { |
| 582 | − m_fileLock.ExitReadLock(); | |
| 583 | − | |
| 584 | − if ((object)fileStream != null) | |
| 585 | − fileStream.Close(); | |
| 574 | + RetrySynchronizedEvent(new TimeoutException("Timeout waiting to acquire write lock for local cache"), ReadEvent); | |
| 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 | 589 | else |
| 594 | 590 | { |
| 595 | − // File doesn't exist, create ane empty array representing a zero-length file | |
| 596 | − m_fileData = new byte[0]; | |
| 597 | − | |
| 598 | − // Release any threads waiting for file data | |
| 599 | − m_loadIsReady.Set(); | |
| 591 | + RetrySynchronizedEvent(new TimeoutException("Timeout waiting to acquire read lock for " + m_fileName), ReadEvent); | |
| 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 | 603 | catch (ThreadAbortException) |
| 604 | 604 | { |
| @@ -618,34 +618,35 @@ namespace GSF.IO · 13 unmodified lines | ||
| 618 | 618 | /// <param name="eventType">Event type to retry.</param> |
| 619 | 619 | private void RetrySynchronizedEvent(Exception ex, int eventType) |
| 620 | 620 | { |
| 621 | + if (m_disposed) | |
| 622 | + return; | |
| 623 | + | |
| 621 | 624 | // A retry is only being initiating for basic file I/O or locking errors, all other errors will initiate an unhandled |
| 622 | 625 | // exception causing system exit. It would be an error, IMO, for the system to create values then not be able to load |
| 623 | 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 | |
| 627 | − if (DateTime.UtcNow.Ticks - m_lastRetryTime > (long)Ticks.FromMilliseconds(m_retryTimer.Interval * m_maximumRetryAttempts)) | |
| 628 | − { | |
| 629 | − // Significant time has passed since last retry, so we reset counter | |
| 630 | − m_retryCount = 0; | |
| 631 | − m_lastRetryTime = DateTime.UtcNow.Ticks; | |
| 632 | − } | |
| 633 | − else | |
| 634 | − { | |
| 635 | − m_retryCount++; | |
| 631 | + // Significant time has passed since last retry, so we reset counter | |
| 632 | + m_retryCount = 0; | |
| 633 | + m_lastRetryTime = DateTime.UtcNow.Ticks; | |
| 634 | + } | |
| 635 | + else | |
| 636 | + { | |
| 637 | + m_retryCount++; | |
| 636 | 638 | |
| 637 | − if (m_retryCount >= m_maximumRetryAttempts) | |
| 638 | − throw new UnauthorizedAccessException("Failed to " + (eventType == WriteEvent ? "write data to " : "read data from ") + m_fileName + " after " + m_maximumRetryAttempts + " attempts: " + ex.Message, ex); | |
| 639 | − } | |
| 639 | + if (m_retryCount >= m_maximumRetryAttempts) | |
| 640 | + throw new UnauthorizedAccessException("Failed to " + (eventType == WriteEvent ? "write data to " : "read data from ") + m_fileName + " after " + m_maximumRetryAttempts + " attempts: " + ex.Message, ex); | |
| 641 | + } | |
| 640 | 642 | |
| 641 | − // Technically the interprocess mutex will handle serialized access to the file, but if the OS or other process | |
| 642 | − // not participating with the mutex has the file locked, all we can do is queue up a retry for this event. | |
| 643 | − lock (m_retryQueue) | |
| 644 | − { | |
| 645 | − m_retryQueue[eventType] = true; | |
| 646 | − } | |
| 647 | − m_retryTimer.Start(); | |
| 643 | + // Technically the inter-process mutex will handle serialized access to the file, but if the OS or other process | |
| 644 | + // not participating with the mutex has the file locked, all we can do is queue up a retry for this event. | |
| 645 | + lock (m_retryQueue) | |
| 646 | + { | |
| 647 | + m_retryQueue[eventType] = true; | |
| 648 | 648 | } |
| 649 | + m_retryTimer.Start(); | |
| 649 | 650 | } |
| 650 | 651 | |
| 651 | 652 | /// <summary> |
| @@ -653,34 +654,34 @@ namespace GSF.IO · 1 unmodified line | ||
| 653 | 654 | /// </summary> |
| 654 | 655 | private void m_retryTimer_Elapsed(object sender, ElapsedEventArgs e) |
| 655 | 656 | { |
| 656 | − if (!m_disposed) | |
| 657 | − { | |
| 658 | − WaitCallback callBackEvent = null; | |
| 657 | + if (m_disposed) | |
| 658 | + return; | |
| 659 | 659 | |
| 660 | − lock (m_retryQueue) | |
| 661 | − { | |
| 662 | − // Reads should always occur first since you may need to load any | |
| 663 | − // newly written data before saving new data. Users can override | |
| 664 | − // load and save behavior to "merge" data sets if needed. | |
| 665 | − if (m_retryQueue[ReadEvent]) | |
| 666 | − { | |
| 667 | − callBackEvent = SynchronizedRead; | |
| 668 | − m_retryQueue[ReadEvent] = false; | |
| 669 | − } | |
| 670 | − else if (m_retryQueue[WriteEvent]) | |
| 671 | − { | |
| 672 | − callBackEvent = SynchronizedWrite; | |
| 673 | − m_retryQueue[WriteEvent] = false; | |
| 674 | − } | |
| 660 | + WaitCallback callBackEvent = null; | |
| 675 | 661 | |
| 676 | − // If any events remain queued for retry, start timer for next event | |
| 677 | − if (m_retryQueue.Any(true)) | |
| 678 | − m_retryTimer.Start(); | |
| 662 | + lock (m_retryQueue) | |
| 663 | + { | |
| 664 | + // Reads should always occur first since you may need to load any | |
| 665 | + // newly written data before saving new data. Users can override | |
| 666 | + // load and save behavior to "merge" data sets if needed. | |
| 667 | + if (m_retryQueue[ReadEvent]) | |
| 668 | + { | |
| 669 | + callBackEvent = SynchronizedRead; | |
| 670 | + m_retryQueue[ReadEvent] = false; | |
| 671 | + } | |
| 672 | + else if (m_retryQueue[WriteEvent]) | |
| 673 | + { | |
| 674 | + callBackEvent = SynchronizedWrite; | |
| 675 | + m_retryQueue[WriteEvent] = false; | |
| 679 | 676 | } |
| 680 | 677 | |
| 681 | − if ((object)callBackEvent != null) | |
| 682 | − ThreadPool.QueueUserWorkItem(callBackEvent); | |
| 678 | + // If any events remain queued for retry, start timer for next event | |
| 679 | + if (m_retryQueue.Any(true)) | |
| 680 | + m_retryTimer.Start(); | |
| 683 | 681 | } |
| 682 | + | |
| 683 | + if ((object)callBackEvent != null) | |
| 684 | + ThreadPool.QueueUserWorkItem(callBackEvent); | |
| 684 | 685 | } |
| 685 | 686 | |
| 686 | 687 | /// <summary> |
| @@ -428,7 +428,7 @@ namespace GSF.Security.Cryptography · 427 unmodified lines | ||
| 428 | 428 | static Cipher() |
| 429 | 429 | { |
| 430 | 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 | 433 | KeyIVCache localKeyIVCache; |
| 434 | 434 | string localCacheFileName = DefaultCacheFileName; |
| @@ -447,7 +447,7 @@ namespace GSF.Security.Cryptography · 12 unmodified lines | ||
| 447 | 447 | retryDelayInterval = settings["CacheRetryDelayInterval"].ValueAs(retryDelayInterval); |
| 448 | 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 | 451 | s_managedEncryption = (Registry.GetValue(fipsKeyNew, "Enabled", 0) ?? Registry.GetValue(fipsKeyOld, "FIPSAlgorithmPolicy", 0)).ToString() == "0"; |
| 452 | 452 | |
| 453 | 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 | 473 | try |
| 474 | 474 | { |
| 475 | 475 | // Validate that user has write access to the local cryptographic cache folder |
| 476 | −#if MONO | |
| 477 | − string tempFile = FilePath.GetDirectoryName(localCacheFileName) + Guid.NewGuid().ToString() + ".tmp"; | |
| 476 | + string tempFile = FilePath.GetDirectoryName(localCacheFileName) + Guid.NewGuid() + ".tmp"; | |
| 478 | 477 | |
| 479 | 478 | using (File.Create(tempFile)) |
| 480 | 479 | { |
| @@ -482,9 +481,6 @@ namespace GSF.Security.Cryptography · 1 unmodified line | ||
| 482 | 481 | |
| 483 | 482 | if (File.Exists(tempFile)) |
| 484 | 483 | File.Delete(tempFile); |
| 485 | −#else | |
| 486 | − Directory.GetAccessControl(FilePath.GetDirectoryName(localCacheFileName)); | |
| 487 | −#endif | |
| 488 | 484 | |
| 489 | 485 | // No access issues exist, use local cache as the primary cryptographic key and initialization vector cache |
| 490 | 486 | s_keyIVCache = localKeyIVCache; |
| @@ -87,7 +87,7 @@ namespace GSF.Threading · 86 unmodified lines | ||
| 87 | 87 | // Create a mutex name that is specific to an object (e.g., a path and file name). |
| 88 | 88 | // Note that we use GetPasswordHash to create a short common name for the name parameter |
| 89 | 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 | 92 | // Prefix mutex name with "Global\" such that mutex will apply to all active |
| 93 | 93 | // application sessions in case terminal services is running. |
| @@ -211,7 +211,7 @@ namespace GSF.Threading · 117 unmodified lines | ||
| 211 | 211 | catch (UnauthorizedAccessException) |
| 212 | 212 | { |
| 213 | 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 | 217 | #endif |
| @@ -29,7 +29,7 @@ using System.Threading; · 28 unmodified lines | ||
| 29 | 29 | namespace GSF.Threading |
| 30 | 30 | { |
| 31 | 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 | 33 | /// </summary> |
| 34 | 34 | public class InterprocessReaderWriterLock : IDisposable |
| 35 | 35 | { |
| @@ -43,9 +43,9 @@ namespace GSF.Threading · 7 unmodified lines | ||
| 43 | 43 | public const int DefaultMaximumConcurrentLocks = 10; |
| 44 | 44 | |
| 45 | 45 | // Fields |
| 46 | − private Mutex m_semaphoreLock; // Mutex used to synchronize access to Semaphore | |
| 47 | − private Semaphore m_concurrencyLock; // Semaphore used for reader/writer lock on consumer object | |
| 48 | − private readonly int m_maximumConcurrentLocks; | |
| 46 | + private Mutex m_semaphoreLock; // Mutex used to synchronize access to Semaphore | |
| 47 | + private Semaphore m_concurrencyLock; // Semaphore used for reader/writer lock on consumer object | |
| 48 | + private readonly int m_maximumConcurrentLocks; // Maximum number of concurrent locks before waiting | |
| 49 | 49 | private bool m_disposed; |
| 50 | 50 | |
| 51 | 51 | #endregion |
| @@ -56,7 +56,7 @@ namespace GSF.Threading · 4 unmodified lines | ||
| 56 | 56 | /// Creates a new instance of the <see cref="InterprocessReaderWriterLock"/> associated with the specified |
| 57 | 57 | /// <paramref name="name"/> that identifies a source object needing concurrency locking. |
| 58 | 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 | 60 | public InterprocessReaderWriterLock(string name) |
| 61 | 61 | : this(name, DefaultMaximumConcurrentLocks) |
| 62 | 62 | { |
| @@ -66,7 +66,7 @@ namespace GSF.Threading · 3 unmodified lines | ||
| 66 | 66 | /// Creates a new instance of the <see cref="InterprocessReaderWriterLock"/> associated with the specified |
| 67 | 67 | /// <paramref name="name"/> that identifies a source object needing concurrency locking. |
| 68 | 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 | 70 | /// <param name="maximumConcurrentLocks">Maximum concurrent reader locks to allow.</param> |
| 71 | 71 | /// <remarks> |
| 72 | 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 | 128 | if (disposing) |
| 129 | 129 | { |
| 130 | 130 | if ((object)m_concurrencyLock != null) |
| 131 | + { | |
| 131 | 132 | m_concurrencyLock.Close(); |
| 132 | − m_concurrencyLock = null; | |
| 133 | + m_concurrencyLock = null; | |
| 134 | + } | |
| 133 | 135 | |
| 134 | 136 | if ((object)m_semaphoreLock != null) |
| 137 | + { | |
| 135 | 138 | m_semaphoreLock.Close(); |
| 136 | − m_semaphoreLock = null; | |
| 139 | + m_semaphoreLock = null; | |
| 140 | + } | |
| 137 | 141 | } |
| 138 | 142 | } |
| 139 | 143 | finally |
| @@ -147,7 +151,7 @@ namespace GSF.Threading · 7 unmodified lines | ||
| 147 | 151 | /// Tries to enter the lock in read mode. |
| 148 | 152 | /// </summary> |
| 149 | 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 | 155 | /// One <see cref="ExitReadLock"/> should be called for each <see cref="EnterReadLock"/> or <see cref="TryEnterReadLock"/>. |
| 152 | 156 | /// </remarks> |
| 153 | 157 | public void EnterReadLock() |
| @@ -159,7 +163,7 @@ namespace GSF.Threading · 5 unmodified lines | ||
| 159 | 163 | /// Tries to enter the lock in write mode. |
| 160 | 164 | /// </summary> |
| 161 | 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 | 167 | /// One <see cref="ExitWriteLock"/> should be called for each <see cref="EnterWriteLock"/> or <see cref="TryEnterWriteLock"/>. |
| 164 | 168 | /// </remarks> |
| 165 | 169 | public void EnterWriteLock() |
| @@ -171,7 +175,7 @@ namespace GSF.Threading · 5 unmodified lines | ||
| 171 | 175 | /// Exits read mode and returns the prior read lock count. |
| 172 | 176 | /// </summary> |
| 173 | 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 | 179 | /// One <see cref="ExitReadLock"/> should be called for each <see cref="EnterReadLock"/> or <see cref="TryEnterReadLock"/>. |
| 176 | 180 | /// </remarks> |
| 177 | 181 | public int ExitReadLock() |
| @@ -184,7 +188,7 @@ namespace GSF.Threading · 6 unmodified lines | ||
| 184 | 188 | /// Exits write mode. |
| 185 | 189 | /// </summary> |
| 186 | 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 | 192 | /// One <see cref="ExitWriteLock"/> should be called for each <see cref="EnterWriteLock"/> or <see cref="TryEnterWriteLock"/>. |
| 189 | 193 | /// </remarks> |
| 190 | 194 | public void ExitWriteLock() |
| @@ -199,12 +203,12 @@ namespace GSF.Threading · 8 unmodified lines | ||
| 199 | 203 | /// <param name="millisecondsTimeout">The number of milliseconds to wait, or -1 (<see cref="Timeout.Infinite"/>) to wait indefinitely.</param> |
| 200 | 204 | /// <returns><c>true</c> if the calling thread entered read mode, otherwise, <c>false</c>.</returns> |
| 201 | 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 | 207 | /// One <see cref="ExitReadLock"/> should be called for each <see cref="EnterReadLock"/> or <see cref="TryEnterReadLock"/>. |
| 204 | 208 | /// </remarks> |
| 205 | 209 | public bool TryEnterReadLock(int millisecondsTimeout) |
| 206 | 210 | { |
| 207 | − bool success = false; | |
| 211 | + bool success; | |
| 208 | 212 | |
| 209 | 213 | try |
| 210 | 214 | { |
| @@ -263,7 +267,9 @@ namespace GSF.Threading · 52 unmodified lines | ||
| 263 | 267 | // The only way to get a semaphore slot count is to execute a successful wait and release. |
| 264 | 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 | 274 | int count = m_concurrencyLock.Release(); |
| 269 | 275 | int adjustedTimeout = millisecondsTimeout; |
| @@ -282,7 +288,9 @@ namespace GSF.Threading · 12 unmodified lines | ||
| 282 | 288 | if (adjustedTimeout < 0) |
| 283 | 289 | adjustedTimeout = 0; |
| 284 | 290 | |
| 285 | − if (success = m_concurrencyLock.WaitOne(adjustedTimeout)) | |
| 291 | + success = m_concurrencyLock.WaitOne(adjustedTimeout); | |
| 292 | + | |
| 293 | + if (success) | |
| 286 | 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 | 27 | // Modified database queries to work with Oracle database. |
| 28 | 28 | // 12/20/2012 - Starlynn Danyelle Gilliam |
| 29 | 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 | 36 | using System; |
| 37 | +using System.Collections.Generic; | |
| 34 | 38 | using System.Configuration; |
| 35 | 39 | using System.Data; |
| 36 | 40 | using System.Diagnostics; |
| @@ -39,17 +43,19 @@ using System.Security; · 2 unmodified lines | ||
| 39 | 43 | using System.Text.RegularExpressions; |
| 40 | 44 | using System.Threading; |
| 41 | 45 | using System.Web.Security; |
| 46 | +using GSF.Collections; | |
| 42 | 47 | using GSF.Configuration; |
| 43 | 48 | using GSF.Data; |
| 49 | +using GSF.Identity; | |
| 44 | 50 | |
| 45 | 51 | namespace GSF.Security |
| 46 | 52 | { |
| 47 | 53 | /// <summary> |
| 48 | − /// Represents an <see cref="ISecurityProvider"/> that uses ADO.NET data source (SQL Server, MySQL, Microsoft Access etc) for its | |
| 49 | − /// backend datastore and authenticates internal users against Active Directory and external users against the database. | |
| 54 | + /// Represents an <see cref="ISecurityProvider"/> that uses ADO.NET data source (SQL Server, MySQL, Oracle, etc.) for its | |
| 55 | + /// backend data store and authenticates internal users against Active Directory and external users against the database. | |
| 50 | 56 | /// </summary> |
| 51 | 57 | /// <example> |
| 52 | − /// Required config file entries: | |
| 58 | + /// Required config file entries (automatically added): | |
| 53 | 59 | /// <code> |
| 54 | 60 | /// <![CDATA[ |
| 55 | 61 | /// <?xml version="1.0"?> |
| @@ -88,14 +94,84 @@ namespace GSF.Security · 32 unmodified lines | ||
| 88 | 94 | /// ]]> |
| 89 | 95 | /// </code> |
| 90 | 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 | 154 | public class AdoSecurityProvider : LdapSecurityProvider |
| 92 | 155 | { |
| 93 | 156 | #region [ Members ] |
| 94 | 157 | |
| 95 | 158 | // Constants |
| 96 | − private const int MinimumPasswordLength = 8; | |
| 97 | − private const string PasswordRequirementRegex = "^.*(?=.{8,})(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).*$"; | |
| 98 | − private const string PasswordRequirementError = "Invalid Password: Password must be at least 8 characters; must contain at least 1 number, 1 upper case letter, and 1 lower case letter"; | |
| 159 | + private const string UserAccountTable = "UserAccount"; // Table name for user accounts | |
| 160 | + private const string SecurityGroupTable = "SecurityGroup"; // Table name for security groups | |
| 161 | + private const string SecurityGroupUserAccountTable = "SecurityGroupUserAccount"; // Table name for security group user accounts | |
| 162 | + private const string ApplicationRoleTable = "ApplicationRole"; // Table name for application roles | |
| 163 | + private const string ApplicationRoleUserAccountTable = "ApplicationRoleUserAccount"; // Table name for application role assignments for user accounts | |
| 164 | + private const string ApplicationRoleSecurityGroupTable = "ApplicationRoleSecurityGroup"; // Table name for application role assignments for security groups | |
| 165 | + | |
| 166 | + /// <summary> | |
| 167 | + /// Default regular expression used to validate new database user passwords. | |
| 168 | + /// </summary> | |
| 169 | + public const string DefaultPasswordRequirementsRegex = "^.*(?=.{8,})(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).*$"; | |
| 170 | + | |
| 171 | + /// <summary> | |
| 172 | + /// Default error message displayed when databases users fail regular expression test. | |
| 173 | + /// </summary> | |
| 174 | + public const string DefaultPasswordRequirementsError = "Invalid Password: Password must be at least 8 characters; must contain at least 1 number, 1 upper case letter, and 1 lower case letter"; | |
| 99 | 175 | |
| 100 | 176 | /// <summary> |
| 101 | 177 | /// Defines the provider ID for the <see cref="AdoSecurityProvider"/>. |
| @@ -104,6 +180,8 @@ namespace GSF.Security · 2 unmodified lines | ||
| 104 | 180 | |
| 105 | 181 | private Exception m_lastException; |
| 106 | 182 | private bool m_successfulPassThroughAuthentication; |
| 183 | + private string m_passwordRequirementsRegex; | |
| 184 | + private string m_passwordRequirementsError; | |
| 107 | 185 | |
| 108 | 186 | #endregion |
| 109 | 187 | |
| @@ -122,14 +200,17 @@ namespace GSF.Security · 12 unmodified lines | ||
| 122 | 200 | /// Initializes a new instance of the <see cref="AdoSecurityProvider"/> class. |
| 123 | 201 | /// </summary> |
| 124 | 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> | |
| 126 | − /// <param name="canUpdateData">true if the security provider can update <see cref="UserData"/> in the backend datastore, otherwise false.</param> | |
| 203 | + /// <param name="canRefreshData">true if the security provider can refresh <see cref="UserData"/> from the backend data store, otherwise false.</param> | |
| 204 | + /// <param name="canUpdateData">true if the security provider can update <see cref="UserData"/> in the backend data store, otherwise false.</param> | |
| 127 | 205 | /// <param name="canResetPassword">true if the security provider can reset user password, otherwise false.</param> |
| 128 | 206 | /// <param name="canChangePassword">true if the security provider can change user password, otherwise false.</param> |
| 129 | 207 | protected AdoSecurityProvider(string username, bool canRefreshData, bool canUpdateData, bool canResetPassword, bool canChangePassword) |
| 130 | 208 | : base(username, canRefreshData, canUpdateData, canResetPassword, canChangePassword) |
| 131 | 209 | { |
| 132 | 210 | base.ConnectionString = "Eval(systemSettings.ConnectionString)"; |
| 211 | + | |
| 212 | + m_passwordRequirementsRegex = DefaultPasswordRequirementsRegex; | |
| 213 | + m_passwordRequirementsError = DefaultPasswordRequirementsError; | |
| 133 | 214 | } |
| 134 | 215 | |
| 135 | 216 | #endregion |
| @@ -137,22 +218,18 @@ namespace GSF.Security · 1 unmodified line | ||
| 137 | 218 | #region [ Properties ] |
| 138 | 219 | |
| 139 | 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 | 222 | /// </summary> |
| 142 | 223 | public override bool CanUpdateData |
| 143 | 224 | { |
| 144 | 225 | get |
| 145 | 226 | { |
| 227 | + // Data update supported on external user accounts. | |
| 146 | 228 | if (UserData.IsDefined && UserData.IsExternal) |
| 147 | − { | |
| 148 | − // Data update supported on external user accounts. | |
| 149 | 229 | return true; |
| 150 | − } | |
| 151 | − else | |
| 152 | − { | |
| 153 | − // Data update not supported on internal user accounts. | |
| 154 | − return false; | |
| 155 | − } | |
| 230 | + | |
| 231 | + // Data update not supported on internal user accounts. | |
| 232 | + return false; | |
| 156 | 233 | } |
| 157 | 234 | } |
| 158 | 235 | |
| @@ -186,8 +263,14 @@ namespace GSF.Security · 27 unmodified lines | ||
| 186 | 263 | // Make sure default settings exist |
| 187 | 264 | ConfigurationFile config = ConfigurationFile.Current; |
| 188 | 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 | 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 | 276 | /// <summary> |
| @@ -208,152 +291,210 @@ namespace GSF.Security · 14 unmodified lines | ||
| 208 | 291 | // Table1: Information about the user. |
| 209 | 292 | // Table2: Groups the user is a member of. |
| 210 | 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) | |
| 232 | − { | |
| 233 | − // User doesn't exist in the database, however, user may exist in an NT authentication group which may have an explict role assignment. To test for this case | |
| 234 | − // we make the assumption that this is a Windows authenticated user and test for rights within groups | |
| 235 | − UserData.IsDefined = true; | |
| 236 | − UserData.IsExternal = false; | |
| 237 | − } | |
| 238 | − else | |
| 239 | − { | |
| 240 | − userDataRow = userDataTable.Rows[0]; | |
| 241 | − UserData.IsDefined = true; | |
| 242 | − UserData.IsExternal = !Convert.ToBoolean(userDataRow["UseADAuthentication"]); | |
| 243 | − } | |
| 311 | + // Validate that needed security tables exist in the data set | |
| 312 | + const string SecurityContextError = "Failed to load a valid security context. Cannot proceed with user authentication."; | |
| 244 | 313 | |
| 245 | − if (UserData.IsExternal && (object)userDataRow != null) | |
| 246 | − { | |
| 247 | − if (string.IsNullOrEmpty(UserData.LoginID)) | |
| 248 | − UserData.LoginID = UserData.Username; | |
| 314 | + if ((object)securityContext == null) | |
| 315 | + throw new SecurityException(SecurityContextError); | |
| 249 | 316 | |
| 250 | − if (!Convert.IsDBNull(userDataRow["Password"])) | |
| 251 | − UserData.Password = Convert.ToString(userDataRow["Password"]); | |
| 317 | + foreach (string securityTable in s_securityTables) | |
| 318 | + { | |
| 319 | + if (!securityContext.Tables.Contains(securityTable)) | |
| 320 | + throw new SecurityException(SecurityContextError); | |
| 321 | + } | |
| 252 | 322 | |
| 253 | − if (!Convert.IsDBNull(userDataRow["FirstName"])) | |
| 254 | − UserData.FirstName = Convert.ToString(userDataRow["FirstName"]); | |
| 323 | + DataRow userAccount = null; | |
| 324 | + Guid userAccountID = Guid.Empty; | |
| 325 | + string userSID = UserInfo.AccountNameToSID(UserData.Username); | |
| 255 | 326 | |
| 256 | − if (!Convert.IsDBNull(userDataRow["LastName"])) | |
| 257 | − UserData.LastName = Convert.ToString(userDataRow["LastName"]); | |
| 327 | + // Filter user account data for the current user | |
| 328 | + DataRow[] userAccounts = securityContext.Tables[UserAccountTable].Select(string.Format("Name = '{0}'", userSID)); | |
| 258 | 329 | |
| 259 | − if (!Convert.IsDBNull(userDataRow["Phone"])) | |
| 260 | − UserData.PhoneNumber = Convert.ToString(userDataRow["Phone"]); | |
| 330 | + if (userAccounts.Length == 0) | |
| 331 | + { | |
| 332 | + // User doesn't exist in the database, however, user may exist in an NT authentication group which | |
| 333 | + // may have an explicit role assignment. To test for this case we make the assumption that this is | |
| 334 | + // a Windows authenticated user and test for rights within groups | |
| 335 | + UserData.IsDefined = true; | |
| 336 | + UserData.IsExternal = false; | |
| 337 | + } | |
| 338 | + else | |
| 339 | + { | |
| 340 | + userAccount = userAccounts[0]; | |
| 341 | + UserData.IsDefined = true; | |
| 342 | + UserData.IsExternal = !Convert.ToBoolean(userAccount["UseADAuthentication"]); | |
| 343 | + userAccountID = Guid.Parse(Convert.ToString(userAccount["ID"])); | |
| 344 | + } | |
| 261 | 345 | |
| 262 | − if (!Convert.IsDBNull(userDataRow["Email"])) | |
| 263 | − UserData.EmailAddress = Convert.ToString(userDataRow["Email"]); | |
| 346 | + if (UserData.IsExternal && (object)userAccount != null) | |
| 347 | + { | |
| 348 | + // Load database user details | |
| 349 | + if (string.IsNullOrEmpty(UserData.LoginID)) | |
| 350 | + UserData.LoginID = UserData.Username; | |
| 264 | 351 | |
| 265 | − if (!Convert.IsDBNull(userDataRow["ChangePasswordOn"])) | |
| 266 | − UserData.PasswordChangeDateTime = Convert.ToDateTime(userDataRow["ChangePasswordOn"]); | |
| 352 | + if (!Convert.IsDBNull(userAccount["Password"])) | |
| 353 | + UserData.Password = Convert.ToString(userAccount["Password"]); | |
| 267 | 354 | |
| 268 | − if (!Convert.IsDBNull(userDataRow["CreatedOn"])) | |
| 269 | − UserData.AccountCreatedDateTime = Convert.ToDateTime(userDataRow["CreatedOn"]); | |
| 355 | + if (!Convert.IsDBNull(userAccount["FirstName"])) | |
| 356 | + UserData.FirstName = Convert.ToString(userAccount["FirstName"]); | |
| 270 | 357 | |
| 271 | − // For possible future use: | |
| 272 | − //if (!Convert.IsDBNull(userDataRow["UserCompanyName"])) | |
| 273 | − // UserData.CompanyName = Convert.ToString(userDataRow["UserCompanyName"]); | |
| 274 | − //if (!Convert.IsDBNull(userDataRow["UserSecurityQuestion"])) | |
| 275 | − // UserData.SecurityQuestion = Convert.ToString(userDataRow["UserSecurityQuestion"]); | |
| 276 | − //if (!Convert.IsDBNull(userDataRow["UserSecurityAnswer"])) | |
| 277 | − // UserData.SecurityAnswer = Convert.ToString(userDataRow["UserSecurityAnswer"]); | |
| 278 | − } | |
| 279 | − else | |
| 280 | − { | |
| 281 | − // Load implicitly assigned groups - this happens via NT user groups that get loaded into user data | |
| 282 | − // group collection. When group definitions are defined with the same name as their NT equivalents, | |
| 283 | − // this will allow automatic external group management from within NT group management (AD or local). | |
| 284 | − base.RefreshData(UserData.Groups, ProviderID); | |
| 285 | − } | |
| 358 | + if (!Convert.IsDBNull(userAccount["LastName"])) | |
| 359 | + UserData.LastName = Convert.ToString(userAccount["LastName"]); | |
| 286 | 360 | |
| 287 | − // Administrator can lock out NT user as well as database-only user via database | |
| 288 | − if (!UserData.IsLockedOut && userDataRow != null && !Convert.IsDBNull(userDataRow["LockedOut"])) | |
| 289 | − UserData.IsLockedOut = Convert.ToBoolean(userDataRow["LockedOut"]); | |
| 361 | + if (!Convert.IsDBNull(userAccount["Phone"])) | |
| 362 | + UserData.PhoneNumber = Convert.ToString(userAccount["Phone"]); | |
| 290 | 363 | |
| 291 | − // Load explicitly assigned groups | |
| 292 | − query = database.ParameterizedQueryString("SELECT SecurityGroupID, SecurityGroupName, SecurityGroupDescription FROM SecurityGroupUserAccountDetail WHERE UserName = {0}", "name"); | |
| 364 | + if (!Convert.IsDBNull(userAccount["Email"])) | |
| 365 | + UserData.EmailAddress = Convert.ToString(userAccount["Email"]); | |
| 293 | 366 | |
| 294 | − using (IDataReader reader = dbConnection.ExecuteReader(query, UserData.Username)) | |
| 295 | − { | |
| 296 | − userGroupDataTable.Load(reader); | |
| 297 | − } | |
| 367 | + if (!Convert.IsDBNull(userAccount["ChangePasswordOn"])) | |
| 368 | + UserData.PasswordChangeDateTime = Convert.ToDateTime(userAccount["ChangePasswordOn"]); | |
| 298 | 369 | |
| 299 | − foreach (DataRow group in userGroupDataTable.Rows) | |
| 300 | − { | |
| 301 | − if (!Convert.IsDBNull(group["SecurityGroupName"])) | |
| 302 | − { | |
| 303 | − groupName = Convert.ToString(group["SecurityGroupName"]); | |
| 370 | + if (!Convert.IsDBNull(userAccount["CreatedOn"])) | |
| 371 | + UserData.AccountCreatedDateTime = Convert.ToDateTime(userAccount["CreatedOn"]); | |
| 304 | 372 | |
| 305 | − if (!UserData.Groups.Contains(groupName, StringComparer.InvariantCultureIgnoreCase)) | |
| 306 | − UserData.Groups.Add(groupName); | |
| 307 | − } | |
| 308 | − } | |
| 373 | + // For possible future use: | |
| 374 | + //if (!Convert.IsDBNull(userDataRow["UserCompanyName"])) | |
| 375 | + // UserData.CompanyName = Convert.ToString(userDataRow["UserCompanyName"]); | |
| 376 | + //if (!Convert.IsDBNull(userDataRow["UserSecurityQuestion"])) | |
| 377 | + // UserData.SecurityQuestion = Convert.ToString(userDataRow["UserSecurityQuestion"]); | |
| 378 | + //if (!Convert.IsDBNull(userDataRow["UserSecurityAnswer"])) | |
| 379 | + // UserData.SecurityAnswer = Convert.ToString(userDataRow["UserSecurityAnswer"]); | |
| 380 | + } | |
| 381 | + else | |
| 382 | + { | |
| 383 | + // Load implicitly assigned groups - this happens via user's NT/AD groups that get loaded into the | |
| 384 | + // user data group collection. When database group definitions are defined with the same name as | |
| 385 | + // their NT/AD equivalents, this will allow automatic external group management. | |
| 386 | + base.RefreshData(UserData.Groups, ProviderID); | |
| 387 | + } | |
| 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 | |
| 313 | − query = database.ParameterizedQueryString("SELECT ApplicationRoleID, ApplicationRoleName, ApplicationRoleDescription FROM AppRoleUserAccountDetail WHERE UserName = {0}", "name"); | |
| 393 | + // At this point an NT/AD based user will have a list of groups that is known to be available to the user. A database | |
| 394 | + // user will have no groups defined yet. The next step will be to load any explicitly assigned groups the user is a | |
| 395 | + // member of. Users can be explicitly assigned to any group, database or NT/AD group. This allows flexibility for | |
| 396 | + // local database users to be assigned to an NT/AD group (in the database) as well as NT/AD users to be a member of | |
| 397 | + // local database groups - allowing customizable role assignments to be fully managed via groups. | |
| 314 | 398 | |
| 315 | − using (IDataReader reader = dbConnection.ExecuteReader(query, UserData.Username)) | |
| 316 | − { | |
| 317 | − userRoleDataTable.Load(reader); | |
| 318 | − } | |
| 399 | + if (userAccountID != Guid.Empty) | |
| 400 | + { | |
| 401 | + // Filter explicitly assigned security groups for current user | |
| 402 | + DataRow[] userGroups = securityContext.Tables[SecurityGroupUserAccountTable].Select(string.Format("UserAccountID = '{0}'", userAccountID)); | |
| 319 | 403 | |
| 320 | − if (userRoleDataTable.Rows.Count == 0) | |
| 404 | + foreach (DataRow row in userGroups) | |
| 321 | 405 | { |
| 322 | − // Load implicitly assigned roles | |
| 323 | − query = database.ParameterizedQueryString("SELECT ApplicationRoleID, ApplicationRoleName, ApplicationRoleDescription FROM AppRoleSecurityGroupDetail WHERE SecurityGroupName = {0}", "groupName"); | |
| 406 | + // Locate associated security group record | |
| 407 | + DataRow[] securityGroups = securityContext.Tables[SecurityGroupTable].Select(string.Format("ID = '{0}'", row["SecurityGroupID"])); | |
| 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)) | |
| 348 | − UserData.Roles.Add(roleName); | |
| 487 | + if (!UserData.Roles.Contains(roleName, StringComparer.OrdinalIgnoreCase)) | |
| 488 | + UserData.Roles.Add(roleName); | |
| 489 | + } | |
| 349 | 490 | } |
| 350 | 491 | } |
| 492 | + } | |
| 351 | 493 | |
| 352 | − // Cache last user roles | |
| 353 | − ThreadPool.QueueUserWorkItem(CacheLastUserRole); | |
| 494 | + // Cache last user roles | |
| 495 | + ThreadPool.QueueUserWorkItem(CacheLastUserRoles); | |
| 354 | 496 | |
| 355 | − return true; | |
| 356 | − } | |
| 497 | + return true; | |
| 357 | 498 | } |
| 358 | 499 | catch (Exception ex) |
| 359 | 500 | { |
| @@ -410,7 +551,7 @@ namespace GSF.Security · 50 unmodified lines | ||
| 410 | 551 | { |
| 411 | 552 | Password = password; |
| 412 | 553 | |
| 413 | − // Authenticate against backend datastore | |
| 554 | + // Authenticate against backend data store | |
| 414 | 555 | UserData.IsAuthenticated = |
| 415 | 556 | UserData.Password == password || |
| 416 | 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 | 587 | if ((object)authenticationException != null) |
| 447 | 588 | { |
| 448 | 589 | m_lastException = authenticationException; |
| @@ -454,7 +595,7 @@ namespace GSF.Security · 5 unmodified lines | ||
| 454 | 595 | } |
| 455 | 596 | |
| 456 | 597 | /// <summary> |
| 457 | − /// Changes user password in the backend datastore. | |
| 598 | + /// Changes user password in the backend data store. | |
| 458 | 599 | /// </summary> |
| 459 | 600 | /// <param name="oldPassword">User's current password.</param> |
| 460 | 601 | /// <param name="newPassword">User's new password.</param> |
| @@ -472,22 +613,23 @@ namespace GSF.Security · 11 unmodified lines | ||
| 472 | 613 | if (oldPassword == newPassword) |
| 473 | 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 | 617 | if (!UserData.IsExternal) |
| 477 | 618 | return base.ChangePassword(oldPassword, newPassword); |
| 478 | 619 | |
| 479 | 620 | // Verify old password. |
| 480 | 621 | UserData.PasswordChangeDateTime = DateTime.MinValue; |
| 622 | + | |
| 481 | 623 | if (!Authenticate(oldPassword)) |
| 482 | 624 | return false; |
| 483 | 625 | |
| 484 | 626 | // Verify new password. |
| 485 | − if (!Regex.IsMatch(newPassword, PasswordRequirementRegex)) | |
| 486 | − throw new SecurityException(PasswordRequirementError); | |
| 627 | + if (!Regex.IsMatch(newPassword, m_passwordRequirementsRegex)) | |
| 628 | + throw new SecurityException(m_passwordRequirementsError); | |
| 487 | 629 | |
| 488 | 630 | using (IDbConnection dbConnection = (new AdoDataConnection(SettingsCategory)).Connection) |
| 489 | 631 | { |
| 490 | − if (dbConnection == null) | |
| 632 | + if ((object)dbConnection == null) | |
| 491 | 633 | return false; |
| 492 | 634 | |
| 493 | 635 | bool oracle = dbConnection.GetType().Name == "OracleConnection"; |
| @@ -550,7 +692,7 @@ namespace GSF.Security · 56 unmodified lines | ||
| 550 | 692 | /// <returns>true if logging was successful, otherwise false.</returns> |
| 551 | 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 | 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 | 698 | EventLogEntryType entryType = loginSuccess ? EventLogEntryType.SuccessAudit : EventLogEntryType.FailureAudit; |
| @@ -589,7 +731,7 @@ namespace GSF.Security · 32 unmodified lines | ||
| 589 | 731 | } |
| 590 | 732 | |
| 591 | 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 | 735 | /// </summary> |
| 594 | 736 | /// <param name="source">Source of the exception.</param> |
| 595 | 737 | /// <param name="message">Detailed description of the exception.</param> |
| @@ -631,50 +773,43 @@ namespace GSF.Security · 35 unmodified lines | ||
| 631 | 773 | return configSettings["LdapPath"].Value; |
| 632 | 774 | } |
| 633 | 775 | |
| 634 | − // Cache last user role and monitor for changes | |
| 635 | − private void CacheLastUserRole(object state) | |
| 776 | + // Cache last user roles and monitor for changes | |
| 777 | + private void CacheLastUserRoles(object state) | |
| 636 | 778 | { |
| 637 | 779 | try |
| 638 | 780 | { |
| 639 | − // Using an interprocess cache for user roles | |
| 781 | + // Using an inter-process cache for user roles | |
| 640 | 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 | |
| 644 | − cachedRole = userRoleCache[UserData.Username]; | |
| 786 | + // Retrieve current user roles | |
| 787 | + currentRoles = new HashSet<string>(UserData.Roles, StringComparer.OrdinalIgnoreCase); | |
| 645 | 788 | |
| 646 | − // Determine current role with maximum rights for user | |
| 647 | − string[] roles = UserData.Roles.ToArray(); | |
| 648 | − | |
| 649 | − // Technically a user can be granted multiple roles, pick the highest ordered role | |
| 650 | − if (roles.Contains("Administrator", StringComparer.InvariantCultureIgnoreCase)) | |
| 651 | − currentRole = "Administrator"; | |
| 652 | − else if (roles.Contains("Editor", StringComparer.InvariantCultureIgnoreCase)) | |
| 653 | − currentRole = "Editor"; | |
| 654 | − else | |
| 655 | − currentRole = "Viewer"; | |
| 789 | + // Attempt to retrieve cached user roles | |
| 790 | + cachedRoles = userRoleCache[UserData.Username]; | |
| 656 | 791 | |
| 657 | − bool roleChanged = false; | |
| 792 | + bool rolesChanged = false; | |
| 658 | 793 | string message; |
| 659 | 794 | EventLogEntryType entryType; |
| 660 | 795 | |
| 661 | − if (string.IsNullOrEmpty(cachedRole)) | |
| 796 | + if ((object)cachedRoles == null || cachedRoles.Length == 0) | |
| 662 | 797 | { |
| 663 | 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 | 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 | 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 | 807 | entryType = EventLogEntryType.Warning; |
| 673 | − roleChanged = true; | |
| 808 | + rolesChanged = true; | |
| 674 | 809 | } |
| 675 | 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 | 813 | entryType = EventLogEntryType.SuccessAudit; |
| 679 | 814 | } |
| 680 | 815 | |
| @@ -689,8 +824,8 @@ namespace GSF.Security · 8 unmodified lines | ||
| 689 | 824 | } |
| 690 | 825 | |
| 691 | 826 | // If role has changed, update cache |
| 692 | − if (roleChanged) | |
| 693 | − userRoleCache[UserData.Username] = currentRole; | |
| 827 | + if (rolesChanged) | |
| 828 | + userRoleCache[UserData.Username] = currentRoles.ToArray(); | |
| 694 | 829 | } |
| 695 | 830 | catch (Exception ex) |
| 696 | 831 | { |
| @@ -706,5 +841,75 @@ namespace GSF.Security · 9 unmodified lines | ||
| 706 | 841 | } |
| 707 | 842 | |
| 708 | 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 | 143 | </ItemGroup> |
| 144 | 144 | <ItemGroup> |
| 145 | 145 | <Compile Include="AdoSecurityProvider.cs" /> |
| 146 | + <Compile Include="AdoSecurityCache.cs" /> | |
| 146 | 147 | <Compile Include="UserRoleCache.cs" /> |
| 147 | 148 | <Compile Include="LdapSecurityProvider.cs" /> |
| 148 | 149 | <Compile Include="RestrictAccessAttribute.cs" /> |
| @@ -285,7 +285,7 @@ namespace GSF.Security · 284 unmodified lines | ||
| 285 | 285 | // Load settings from the specified category. |
| 286 | 286 | ConfigurationFile config = ConfigurationFile.Current; |
| 287 | 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 | 289 | settings.Add("CacheRetryDelayInterval", m_cacheRetryDelayInterval, "Wait interval, in milliseconds, before retrying load of user data cache."); |
| 290 | 290 | settings.Add("CacheMaximumRetryAttempts", m_cacheMaximumRetryAttempts, "Maximum retry attempts allowed for loading user data cache."); |
| 291 | 291 | EnableOfflineCaching = settings["EnableOfflineCaching"].ValueAs(m_enableOfflineCaching); |
| @@ -312,15 +312,19 @@ namespace GSF.Security · 20 unmodified lines | ||
| 312 | 312 | { |
| 313 | 313 | // Validate with current thread principal. |
| 314 | 314 | m_windowsPrincipal = Thread.CurrentPrincipal as WindowsPrincipal; |
| 315 | − UserData.IsAuthenticated = m_windowsPrincipal != null && !string.IsNullOrEmpty(UserData.LoginID) && | |
| 316 | − string.Compare(m_windowsPrincipal.Identity.Name, UserData.LoginID, true) == 0 && m_windowsPrincipal.Identity.IsAuthenticated; | |
| 315 | + | |
| 316 | + UserData.IsAuthenticated = | |
| 317 | + (object)m_windowsPrincipal != null && | |
| 318 | + !string.IsNullOrEmpty(UserData.LoginID) && | |
| 319 | + string.Compare(m_windowsPrincipal.Identity.Name, UserData.LoginID, StringComparison.OrdinalIgnoreCase) == 0 && | |
| 320 | + m_windowsPrincipal.Identity.IsAuthenticated; | |
| 317 | 321 | } |
| 318 | 322 | else |
| 319 | 323 | { |
| 320 | 324 | // Validate by performing network logon. |
| 321 | 325 | string[] userParts = UserData.LoginID.Split('\\'); |
| 322 | 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 | 330 | return UserData.IsAuthenticated; |
| @@ -332,10 +336,10 @@ namespace GSF.Security · 5 unmodified lines | ||
| 332 | 336 | /// <returns>true if <see cref="SecurityProviderBase.UserData"/> is refreshed, otherwise false.</returns> |
| 333 | 337 | public override bool RefreshData() |
| 334 | 338 | { |
| 335 | − bool result; | |
| 336 | − | |
| 337 | 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 | 344 | string[] parts; |
| 341 | 345 | |
| @@ -360,7 +364,7 @@ namespace GSF.Security · 18 unmodified lines | ||
| 360 | 364 | /// <returns>true if <see cref="SecurityProviderBase.UserData"/> is refreshed, otherwise false.</returns> |
| 361 | 365 | protected virtual bool RefreshData(List<string> groupCollection, int providerID) |
| 362 | 366 | { |
| 363 | − if (groupCollection == null) | |
| 367 | + if ((object)groupCollection == null) | |
| 364 | 368 | throw new ArgumentNullException("groupCollection"); |
| 365 | 369 | |
| 366 | 370 | if (string.IsNullOrEmpty(UserData.Username)) |
| @@ -393,6 +397,7 @@ namespace GSF.Security · 26 unmodified lines | ||
| 393 | 397 | |
| 394 | 398 | // Create user info object using specified LDAP path if provided |
| 395 | 399 | string ldapPath = GetLdapPath(); |
| 400 | + | |
| 396 | 401 | if (string.IsNullOrEmpty(ldapPath)) |
| 397 | 402 | user = new UserInfo(UserData.Username); |
| 398 | 403 | else |
| @@ -428,7 +433,7 @@ namespace GSF.Security · 29 unmodified lines | ||
| 428 | 433 | groupCollection.Add(groupName); |
| 429 | 434 | } |
| 430 | 435 | |
| 431 | − if (userDataCache != null) | |
| 436 | + if ((object)userDataCache != null) | |
| 432 | 437 | { |
| 433 | 438 | // Cache user data so that information can be loaded later if domain is unavailable |
| 434 | 439 | userDataCache[UserData.LoginID] = UserData; |
| @@ -440,8 +445,9 @@ namespace GSF.Security · 5 unmodified lines | ||
| 440 | 445 | else |
| 441 | 446 | { |
| 442 | 447 | // Attempt to load previously cached user information when domain is offline |
| 443 | − UserData cachedUserData = null; | |
| 444 | − if (userDataCache != null && userDataCache.TryGetUserData(UserData.LoginID, out cachedUserData)) | |
| 448 | + UserData cachedUserData; | |
| 449 | + | |
| 450 | + if ((object)userDataCache != null && userDataCache.TryGetUserData(UserData.LoginID, out cachedUserData)) | |
| 445 | 451 | { |
| 446 | 452 | // Copy relevant cached user information |
| 447 | 453 | UserData.FirstName = cachedUserData.FirstName; |
| @@ -473,19 +479,19 @@ namespace GSF.Security · 25 unmodified lines | ||
| 473 | 479 | } |
| 474 | 480 | finally |
| 475 | 481 | { |
| 476 | − if (user != null) | |
| 482 | + if ((object)user != null) | |
| 477 | 483 | { |
| 478 | 484 | user.PersistSettings = false; |
| 479 | 485 | user.Dispose(); |
| 480 | 486 | } |
| 481 | 487 | |
| 482 | − if (userDataCache != null) | |
| 488 | + if ((object)userDataCache != null) | |
| 483 | 489 | userDataCache.Dispose(); |
| 484 | 490 | } |
| 485 | 491 | } |
| 486 | 492 | |
| 487 | 493 | /// <summary> |
| 488 | − /// Changes user password in the backend datastore. | |
| 494 | + /// Changes user password in the backend data store. | |
| 489 | 495 | /// </summary> |
| 490 | 496 | /// <param name="oldPassword">User's current password.</param> |
| 491 | 497 | /// <param name="newPassword">User's new password.</param> |
| @@ -534,17 +540,17 @@ namespace GSF.Security · 42 unmodified lines | ||
| 534 | 540 | catch (TargetInvocationException ex) |
| 535 | 541 | { |
| 536 | 542 | // Propagate password change error |
| 537 | − if (ex.InnerException == null) | |
| 543 | + if ((object)ex.InnerException == null) | |
| 538 | 544 | throw new SecurityException(ex.Message, ex); |
| 539 | 545 | else |
| 540 | 546 | throw new SecurityException(ex.InnerException.Message, ex); |
| 541 | 547 | } |
| 542 | 548 | finally |
| 543 | 549 | { |
| 544 | − if (user != null) | |
| 550 | + if ((object)user != null) | |
| 545 | 551 | user.Dispose(); |
| 546 | 552 | |
| 547 | − if (context != null) | |
| 553 | + if ((object)context != null) | |
| 548 | 554 | UserInfo.EndImpersonation(context); |
| 549 | 555 | |
| 550 | 556 | RefreshData(); |
| @@ -572,19 +578,14 @@ namespace GSF.Security · 21 unmodified lines | ||
| 572 | 578 | /// <returns>The LDAP path.</returns> |
| 573 | 579 | protected virtual string GetLdapPath() |
| 574 | 580 | { |
| 575 | − if (ConnectionString.StartsWith("LDAP://", StringComparison.InvariantCultureIgnoreCase) || | |
| 576 | − ConnectionString.StartsWith("LDAPS://", StringComparison.InvariantCultureIgnoreCase)) | |
| 577 | − { | |
| 581 | + if (ConnectionString.StartsWith("LDAP://", StringComparison.InvariantCultureIgnoreCase) || ConnectionString.StartsWith("LDAPS://", StringComparison.InvariantCultureIgnoreCase)) | |
| 578 | 582 | return ConnectionString; |
| 579 | − } | |
| 580 | − else | |
| 583 | + | |
| 584 | + foreach (KeyValuePair<string, string> pair in ConnectionString.ParseKeyValuePairs()) | |
| 581 | 585 | { |
| 582 | − foreach (KeyValuePair<string, string> pair in ConnectionString.ParseKeyValuePairs()) | |
| 583 | − { | |
| 584 | − if (pair.Value.StartsWith("LDAP://", StringComparison.InvariantCultureIgnoreCase) || | |
| 585 | − pair.Value.StartsWith("LDAPS://", StringComparison.InvariantCultureIgnoreCase)) | |
| 586 | − return pair.Value; | |
| 587 | − } | |
| 586 | + if (pair.Value.StartsWith("LDAP://", StringComparison.InvariantCultureIgnoreCase) || | |
| 587 | + pair.Value.StartsWith("LDAPS://", StringComparison.InvariantCultureIgnoreCase)) | |
| 588 | + return pair.Value; | |
| 588 | 589 | } |
| 589 | 590 | |
| 590 | 591 | return null; |
| @@ -284,7 +284,7 @@ namespace GSF.Security · 283 unmodified lines | ||
| 284 | 284 | } |
| 285 | 285 | |
| 286 | 286 | /// <summary> |
| 287 | − /// Gets or sets <see cref="SecurePassword"/> as cleartext password. | |
| 287 | + /// Gets or sets <see cref="SecurePassword"/> as clear text password. | |
| 288 | 288 | /// </summary> |
| 289 | 289 | public unsafe string Password |
| 290 | 290 | { |
| @@ -314,7 +314,7 @@ namespace GSF.Security · 23 unmodified lines | ||
| 314 | 314 | { |
| 315 | 315 | SecureString securePassword = new SecureString(); |
| 316 | 316 | |
| 317 | − if (value == null) | |
| 317 | + if ((object)value == null) | |
| 318 | 318 | value = string.Empty; |
| 319 | 319 | |
| 320 | 320 | foreach (char c in value) |
| @@ -621,7 +621,7 @@ namespace GSF.Security · 300 unmodified lines | ||
| 621 | 621 | { |
| 622 | 622 | m_disposed = true; // Prevent duplicate dispose. |
| 623 | 623 | |
| 624 | − if (Disposed != null) | |
| 624 | + if ((object)Disposed != null) | |
| 625 | 625 | Disposed(this, EventArgs.Empty); |
| 626 | 626 | } |
| 627 | 627 | } |
| @@ -25,7 +25,7 @@ · 24 unmodified lines | ||
| 25 | 25 | // 01/14/2011 - Pinal C. Patel |
| 26 | 26 | // Updated field attributes used by XmlSerializer. |
| 27 | 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 | 37 | namespace GSF.Security |
| 38 | 38 | { |
| 39 | 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 | 41 | /// </summary> |
| 42 | 42 | [XmlType(Namespace = ""), DataContract(Namespace = ""), Serializable] |
| 43 | 43 | public class UserData |
| @@ -73,115 +73,191 @@ namespace GSF.Security · 29 unmodified lines | ||
| 73 | 73 | /// <summary> |
| 74 | 74 | /// Gets the user's login ID. |
| 75 | 75 | /// </summary> |
| 76 | − public string LoginID { get; set; } | |
| 76 | + public string LoginID | |
| 77 | + { | |
| 78 | + get; | |
| 79 | + set; | |
| 80 | + } | |
| 77 | 81 | |
| 78 | 82 | /// <summary> |
| 79 | 83 | /// Gets the user's login name. |
| 80 | 84 | /// </summary> |
| 81 | 85 | [XmlElement, DataMember(Order = 0)] |
| 82 | − public string Username { get; set; } | |
| 86 | + public string Username | |
| 87 | + { | |
| 88 | + get; | |
| 89 | + set; | |
| 90 | + } | |
| 83 | 91 | |
| 84 | 92 | /// <summary> |
| 85 | 93 | /// Gets the user's password. |
| 86 | 94 | /// </summary> |
| 87 | 95 | [XmlElement, DataMember(Order = 1)] |
| 88 | − public string Password { get; set; } | |
| 96 | + public string Password | |
| 97 | + { | |
| 98 | + get; | |
| 99 | + set; | |
| 100 | + } | |
| 89 | 101 | |
| 90 | 102 | /// <summary> |
| 91 | 103 | /// Gets the user's first name. |
| 92 | 104 | /// </summary> |
| 93 | 105 | [XmlElement, DataMember(Order = 2)] |
| 94 | − public string FirstName { get; set; } | |
| 106 | + public string FirstName | |
| 107 | + { | |
| 108 | + get; | |
| 109 | + set; | |
| 110 | + } | |
| 95 | 111 | |
| 96 | 112 | /// <summary> |
| 97 | 113 | /// Gets the user's last name. |
| 98 | 114 | /// </summary> |
| 99 | 115 | [XmlElement, DataMember(Order = 3)] |
| 100 | − public string LastName { get; set; } | |
| 116 | + public string LastName | |
| 117 | + { | |
| 118 | + get; | |
| 119 | + set; | |
| 120 | + } | |
| 101 | 121 | |
| 102 | 122 | /// <summary> |
| 103 | 123 | /// Gets the user's company name. |
| 104 | 124 | /// </summary> |
| 105 | 125 | [XmlElement, DataMember(Order = 4)] |
| 106 | − public string CompanyName { get; set; } | |
| 126 | + public string CompanyName | |
| 127 | + { | |
| 128 | + get; | |
| 129 | + set; | |
| 130 | + } | |
| 107 | 131 | |
| 108 | 132 | /// <summary> |
| 109 | 133 | /// Gets the user's phone number. |
| 110 | 134 | /// </summary> |
| 111 | 135 | [XmlElement, DataMember(Order = 5)] |
| 112 | − public string PhoneNumber { get; set; } | |
| 136 | + public string PhoneNumber | |
| 137 | + { | |
| 138 | + get; | |
| 139 | + set; | |
| 140 | + } | |
| 113 | 141 | |
| 114 | 142 | /// <summary> |
| 115 | 143 | /// Gets the user's email address. |
| 116 | 144 | /// </summary> |
| 117 | 145 | [XmlElement, DataMember(Order = 6)] |
| 118 | − public string EmailAddress { get; set; } | |
| 146 | + public string EmailAddress | |
| 147 | + { | |
| 148 | + get; | |
| 149 | + set; | |
| 150 | + } | |
| 119 | 151 | |
| 120 | 152 | /// <summary> |
| 121 | 153 | /// Gets the user's security question. |
| 122 | 154 | /// </summary> |
| 123 | 155 | [XmlElement, DataMember(Order = 7)] |
| 124 | − public string SecurityQuestion { get; set; } | |
| 156 | + public string SecurityQuestion | |
| 157 | + { | |
| 158 | + get; | |
| 159 | + set; | |
| 160 | + } | |
| 125 | 161 | |
| 126 | 162 | /// <summary> |
| 127 | 163 | /// Gets the user's security answer. |
| 128 | 164 | /// </summary> |
| 129 | 165 | [XmlElement, DataMember(Order = 8)] |
| 130 | − public string SecurityAnswer { get; set; } | |
| 166 | + public string SecurityAnswer | |
| 167 | + { | |
| 168 | + get; | |
| 169 | + set; | |
| 170 | + } | |
| 131 | 171 | |
| 132 | 172 | /// <summary> |
| 133 | 173 | /// Gets the UTC date and time when user must change the password. |
| 134 | 174 | /// </summary> |
| 135 | 175 | [XmlElement, DataMember(Order = 9)] |
| 136 | − public DateTime PasswordChangeDateTime { get; set; } | |
| 176 | + public DateTime PasswordChangeDateTime | |
| 177 | + { | |
| 178 | + get; | |
| 179 | + set; | |
| 180 | + } | |
| 137 | 181 | |
| 138 | 182 | /// <summary> |
| 139 | 183 | /// Gets the UTC date and time when user account was created. |
| 140 | 184 | /// </summary> |
| 141 | 185 | [XmlElement, DataMember(Order = 10)] |
| 142 | − public DateTime AccountCreatedDateTime { get; set; } | |
| 186 | + public DateTime AccountCreatedDateTime | |
| 187 | + { | |
| 188 | + get; | |
| 189 | + set; | |
| 190 | + } | |
| 143 | 191 | |
| 144 | 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 | 194 | /// </summary> |
| 147 | 195 | [XmlElement, DataMember(Order = 11)] |
| 148 | − public bool IsDefined { get; set; } | |
| 196 | + public bool IsDefined | |
| 197 | + { | |
| 198 | + get; | |
| 199 | + set; | |
| 200 | + } | |
| 149 | 201 | |
| 150 | 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 | 204 | /// </summary> |
| 153 | 205 | [XmlElement, DataMember(Order = 12)] |
| 154 | − public bool IsExternal { get; set; } | |
| 206 | + public bool IsExternal | |
| 207 | + { | |
| 208 | + get; | |
| 209 | + set; | |
| 210 | + } | |
| 155 | 211 | |
| 156 | 212 | /// <summary> |
| 157 | 213 | /// Gets a boolean value that indicates whether the user account has been disabled. |
| 158 | 214 | /// </summary> |
| 159 | 215 | [XmlElement, DataMember(Order = 13)] |
| 160 | − public bool IsDisabled { get; set; } | |
| 216 | + public bool IsDisabled | |
| 217 | + { | |
| 218 | + get; | |
| 219 | + set; | |
| 220 | + } | |
| 161 | 221 | |
| 162 | 222 | /// <summary> |
| 163 | 223 | /// Gets a boolean value that indicates whether the user account has been locked due to numerous unsuccessful login attempts. |
| 164 | 224 | /// </summary> |
| 165 | 225 | [XmlElement, DataMember(Order = 14)] |
| 166 | − public bool IsLockedOut { get; set; } | |
| 226 | + public bool IsLockedOut | |
| 227 | + { | |
| 228 | + get; | |
| 229 | + set; | |
| 230 | + } | |
| 167 | 231 | |
| 168 | 232 | /// <summary> |
| 169 | 233 | /// Gets a boolean value indicating whether or not the user has been authenticated. |
| 170 | 234 | /// </summary> |
| 171 | 235 | [XmlElement, DataMember(Order = 15)] |
| 172 | − public bool IsAuthenticated { get; set; } | |
| 236 | + public bool IsAuthenticated | |
| 237 | + { | |
| 238 | + get; | |
| 239 | + set; | |
| 240 | + } | |
| 173 | 241 | |
| 174 | 242 | /// <summary> |
| 175 | 243 | /// Gets a read-only list of all the groups the user belongs to. |
| 176 | 244 | /// </summary> |
| 177 | 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 | 252 | /// <summary> |
| 181 | 253 | /// Gets a read-only list of all the roles assigned to the user. |
| 182 | 254 | /// </summary> |
| 183 | 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 | 262 | #endregion |
| 187 | 263 | |
| @@ -24,7 +24,7 @@ · 23 unmodified lines | ||
| 24 | 24 | // Renamed RetryDelayInterval and MaximumRetryAttempts settings persisted to the config file |
| 25 | 25 | // to CacheRetryDelayInterval and CacheMaximumRetryAttempts for clarity. |
| 26 | 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 | 28 | // implementation in case the category has been changed from the default value by the consumer. |
| 29 | 29 | // 08/16/2011 - Pinal C. Patel |
| 30 | 30 | // Modified GetCurrentCache() to just set the FileName property and not the RetryDelayInterval, |
| @@ -46,8 +46,19 @@ using GSF.Threading; · 15 unmodified lines | ||
| 46 | 46 | namespace GSF.Security |
| 47 | 47 | { |
| 48 | 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 | 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 | 62 | public class UserDataCache : InterprocessCache |
| 52 | 63 | { |
| 53 | 64 | #region [ Members ] |
| @@ -59,7 +70,7 @@ namespace GSF.Security · 5 unmodified lines | ||
| 59 | 70 | |
| 60 | 71 | // Fields |
| 61 | 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 | 74 | private int m_providerID; // Unique provider ID used to distinguish cached user data that may be different based on provider |
| 64 | 75 | |
| 65 | 76 | #endregion |
| @@ -108,7 +119,7 @@ namespace GSF.Security · 42 unmodified lines | ||
| 108 | 119 | } |
| 109 | 120 | |
| 110 | 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 | 123 | /// </summary> |
| 113 | 124 | public int ProviderID |
| 114 | 125 | { |
| @@ -183,7 +194,7 @@ namespace GSF.Security · 68 unmodified lines | ||
| 183 | 194 | } |
| 184 | 195 | |
| 185 | 196 | /// <summary> |
| 186 | − /// Initiates interprocess synchronized save of user data cache. | |
| 197 | + /// Initiates inter-process synchronized save of user data cache. | |
| 187 | 198 | /// </summary> |
| 188 | 199 | public override void Save() |
| 189 | 200 | { |
| @@ -195,7 +206,7 @@ namespace GSF.Security · 5 unmodified lines | ||
| 195 | 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 | 210 | FileData = serializedUserDataTable; |
| 200 | 211 | } |
| 201 | 212 | |
| @@ -296,6 +307,5 @@ namespace GSF.Security · 94 unmodified lines | ||
| 296 | 307 | } |
| 297 | 308 | |
| 298 | 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 | 24 | using System; |
| 29 | 25 | using System.Collections.Generic; |
| 30 | 26 | using System.IO; |
| 31 | 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 | 33 | namespace GSF.Security |
| 34 | 34 | { |
| 35 | 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 | 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 | 45 | public class UserRoleCache : InterprocessCache |
| 39 | 46 | { |
| 40 | 47 | #region [ Members ] |
| @@ -45,8 +52,8 @@ namespace GSF.Security · 4 unmodified lines | ||
| 45 | 52 | private const string DefaultCacheFileName = "UserRoleCache.bin"; |
| 46 | 53 | |
| 47 | 54 | // Fields |
| 48 | − private Dictionary<string, string> m_userRoles; // Internal dictionary of serialized user roles | |
| 49 | − private readonly object m_userRolesLock; // Lock object | |
| 55 | + private Dictionary<string, string[]> m_userRoles; // Internal dictionary of serialized user roles | |
| 56 | + private readonly object m_userRolesLock; // Lock object for internal dictionary | |
| 50 | 57 | |
| 51 | 58 | #endregion |
| 52 | 59 | |
| @@ -57,7 +64,7 @@ namespace GSF.Security · 4 unmodified lines | ||
| 57 | 64 | public UserRoleCache(int maximumConcurrentLocks = InterprocessReaderWriterLock.DefaultMaximumConcurrentLocks) |
| 58 | 65 | : base(maximumConcurrentLocks) |
| 59 | 66 | { |
| 60 | − m_userRoles = new Dictionary<string, string>(); | |
| 67 | + m_userRoles = new Dictionary<string, string[]>(); | |
| 61 | 68 | m_userRolesLock = new object(); |
| 62 | 69 | } |
| 63 | 70 | |
| @@ -66,11 +73,11 @@ namespace GSF.Security · 2 unmodified lines | ||
| 66 | 73 | /// <summary> |
| 67 | 74 | /// Gets a copy of the internal user role dictionary. |
| 68 | 75 | /// </summary> |
| 69 | − public Dictionary<string, string> UserRoles | |
| 76 | + public Dictionary<string, string[]> UserRoles | |
| 70 | 77 | { |
| 71 | 78 | get |
| 72 | 79 | { |
| 73 | − Dictionary<string, string> userRoles; | |
| 80 | + Dictionary<string, string[]> userRoles; | |
| 74 | 81 | |
| 75 | 82 | // We wait until the user roles cache is loaded before attempting to access it |
| 76 | 83 | WaitForDataReady(); |
| @@ -78,8 +85,8 @@ namespace GSF.Security · 1 unmodified line | ||
| 78 | 85 | // Wait for thread level lock on dictionary |
| 79 | 86 | lock (m_userRolesLock) |
| 80 | 87 | { |
| 81 | − // Make a copy of the user role table for external use | |
| 82 | − userRoles = new Dictionary<string, string>(m_userRoles); | |
| 88 | + // Make a copy of the user role dictionary for external use | |
| 89 | + userRoles = new Dictionary<string, string[]>(m_userRoles); | |
| 83 | 90 | } |
| 84 | 91 | |
| 85 | 92 | return userRoles; |
| @@ -87,17 +94,17 @@ namespace GSF.Security · 1 unmodified line | ||
| 87 | 94 | } |
| 88 | 95 | |
| 89 | 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 | 98 | /// </summary> |
| 92 | 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> | |
| 94 | − public string this[string userName] | |
| 100 | + /// <returns>Access roles for given <paramref name="userName"/> if found; otherwise <c>null</c>.</returns> | |
| 101 | + public string[] this[string userName] | |
| 95 | 102 | { |
| 96 | 103 | get |
| 97 | 104 | { |
| 98 | − string role; | |
| 99 | − TryGetUserRole(userName, out role); | |
| 100 | − return role; | |
| 105 | + string[] roles; | |
| 106 | + TryGetUserRole(userName, out roles); | |
| 107 | + return roles; | |
| 101 | 108 | } |
| 102 | 109 | set |
| 103 | 110 | { |
| @@ -113,9 +120,9 @@ namespace GSF.Security · 9 unmodified lines | ||
| 113 | 120 | /// Attempts to retrieve access role for given <paramref name="userName"/>. |
| 114 | 121 | /// </summary> |
| 115 | 122 | /// <param name="userName">User name associated with access role to retrieve.</param> |
| 116 | − /// <param name="role">Access role to populate if found.</param> | |
| 117 | − /// <returns><c>true</c> if access role for given <paramref name="userName"/> was retrieved; otherwise <c>false</c>.</returns> | |
| 118 | − public bool TryGetUserRole(string userName, out string role) | |
| 123 | + /// <param name="roles">Access roles to populate if found.</param> | |
| 124 | + /// <returns><c>true</c> if access roles for given <paramref name="userName"/> were retrieved; otherwise <c>false</c>.</returns> | |
| 125 | + public bool TryGetUserRole(string userName, out string[] roles) | |
| 119 | 126 | { |
| 120 | 127 | string hash = HashLoginID(userName); |
| 121 | 128 | bool result; |
| @@ -123,42 +130,42 @@ namespace GSF.Security · 1 unmodified line | ||
| 123 | 130 | // We wait until the cache is loaded before attempting to access it |
| 124 | 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 | 134 | lock (m_userRolesLock) |
| 128 | 135 | { |
| 129 | 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 | 140 | return result; |
| 134 | 141 | } |
| 135 | 142 | |
| 136 | 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 | 145 | /// </summary> |
| 139 | 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 | 148 | /// <remarks> |
| 142 | 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 | 151 | /// otherwise existing entry will be updated. |
| 145 | 152 | /// </para> |
| 146 | 153 | /// <para> |
| 147 | 154 | /// Updates are automatically queued up for serialization so user does not need to call <see cref="Save"/>. |
| 148 | 155 | /// </para> |
| 149 | 156 | /// </remarks> |
| 150 | − public void SaveUserRole(string userName, string role) | |
| 157 | + public void SaveUserRole(string userName, string[] roles) | |
| 151 | 158 | { |
| 152 | 159 | string hash = HashLoginID(userName); |
| 153 | 160 | |
| 154 | 161 | // We wait until the cache is loaded before attempting to access it |
| 155 | 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 | 165 | lock (m_userRolesLock) |
| 159 | 166 | { |
| 160 | − // Assign new user information to user data table | |
| 161 | − m_userRoles[hash] = role; | |
| 167 | + // Assign new user information to user data dictionary | |
| 168 | + m_userRoles[hash] = roles; | |
| 162 | 169 | } |
| 163 | 170 | |
| 164 | 171 | // Queue up a serialization for this new user information |
| @@ -172,7 +179,7 @@ namespace GSF.Security · 7 unmodified lines | ||
| 172 | 179 | public void MergeLeft(UserRoleCache other) |
| 173 | 180 | { |
| 174 | 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 | 184 | // Wait for thread level lock on dictionary |
| 178 | 185 | lock (m_userRolesLock) |
| @@ -192,7 +199,7 @@ namespace GSF.Security · 13 unmodified lines | ||
| 192 | 199 | public void MergeRight(UserRoleCache other) |
| 193 | 200 | { |
| 194 | 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 | 204 | // Wait for thread level lock on dictionary |
| 198 | 205 | lock (m_userRolesLock) |
| @@ -206,7 +213,7 @@ namespace GSF.Security · 7 unmodified lines | ||
| 206 | 213 | } |
| 207 | 214 | |
| 208 | 215 | /// <summary> |
| 209 | − /// Initiates interprocess synchronized save of user role cache. | |
| 216 | + /// Initiates inter-process synchronized save of user role cache. | |
| 210 | 217 | /// </summary> |
| 211 | 218 | public override void Save() |
| 212 | 219 | { |
| @@ -218,7 +225,7 @@ namespace GSF.Security · 5 unmodified lines | ||
| 218 | 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 | 229 | FileData = serializedUserDataTable; |
| 223 | 230 | } |
| 224 | 231 | |
| @@ -248,7 +255,7 @@ namespace GSF.Security · 23 unmodified lines | ||
| 248 | 255 | { |
| 249 | 256 | // Decrypt data that was encrypted local to this machine |
| 250 | 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 | 260 | // Wait for thread level lock on user role dictionary |
| 254 | 261 | lock (m_userRolesLock) |
| @@ -266,7 +273,7 @@ namespace GSF.Security · 11 unmodified lines | ||
| 266 | 273 | /// <param name="userName">User name to hash.</param> |
| 267 | 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 | 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 | 277 | /// in the user roles cache instead of the actual <paramref name="userName"/>. This method allows the |
| 271 | 278 | /// consumer to properly calculate this hash when directly using the user data cache. |
| 272 | 279 | /// </remarks> |
| @@ -324,8 +331,8 @@ namespace GSF.Security · 51 unmodified lines | ||
| 324 | 331 | |
| 325 | 332 | try |
| 326 | 333 | { |
| 327 | − // Validate that user has write access to the local cryptographic cache folder | |
| 328 | − string tempFile = FilePath.GetDirectoryName(localCacheFileName) + Guid.NewGuid().ToString() + ".tmp"; | |
| 334 | + // Validate that user has write access to the local cache folder | |
| 335 | + string tempFile = FilePath.GetDirectoryName(localCacheFileName) + Guid.NewGuid() + ".tmp"; | |
| 329 | 336 | |
| 330 | 337 | using (File.Create(tempFile)) |
| 331 | 338 | { |
| @@ -334,13 +341,13 @@ namespace GSF.Security · 2 unmodified lines | ||
| 334 | 341 | if (File.Exists(tempFile)) |
| 335 | 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 | 345 | currentCache = localUserRoleCache; |
| 339 | 346 | currentCache.AutoSave = true; |
| 340 | 347 | } |
| 341 | 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 | 351 | // use a path where user will have rights |
| 345 | 352 | string userCacheFolder = FilePath.AddPathSuffix(FilePath.GetApplicationDataFolder()); |
| 346 | 353 | string userCacheFileName = userCacheFolder + FilePath.GetFileName(localCacheFileName); |
| @@ -349,11 +356,11 @@ namespace GSF.Security · 2 unmodified lines | ||
| 349 | 356 | if (!Directory.Exists(userCacheFolder)) |
| 350 | 357 | Directory.CreateDirectory(userCacheFolder); |
| 351 | 358 | |
| 352 | − // Copy existing common cryptographic cache if none exists | |
| 359 | + // Copy existing common cache if none exists | |
| 353 | 360 | if (File.Exists(localCacheFileName) && !File.Exists(userCacheFileName)) |
| 354 | 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 | 364 | currentCache = new UserRoleCache |
| 358 | 365 | { |
| 359 | 366 | FileName = userCacheFileName, |
| @@ -367,10 +374,10 @@ namespace GSF.Security · 7 unmodified lines | ||
| 367 | 374 | AutoSave = true |
| 368 | 375 | }; |
| 369 | 376 | |
| 370 | − // Load initial keys | |
| 377 | + // Load initial roles | |
| 371 | 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 | 381 | currentCache.MergeRight(localUserRoleCache); |
| 375 | 382 | } |
| 376 | 383 | |
| @@ -52,6 +52,7 @@ using GSF.Data; · 51 unmodified lines | ||
| 52 | 52 | using GSF.IO; |
| 53 | 53 | using GSF.Net.Security; |
| 54 | 54 | using GSF.Reflection; |
| 55 | +using GSF.Security; | |
| 55 | 56 | using GSF.ServiceProcess; |
| 56 | 57 | using GSF.TimeSeries.Adapters; |
| 57 | 58 | using GSF.Units; |
| @@ -968,6 +969,17 @@ namespace GSF.TimeSeries · 910 unmodified lines | ||
| 968 | 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 | 983 | DisplayStatusMessage("Database configuration successfully loaded.", UpdateType.Information); |
| 972 | 984 | } |
| 973 | 985 | catch (Exception ex) |