diff --git a/ExternalPlugins/SppSecrets/PluginDescriptor.cs b/ExternalPlugins/SppSecrets/PluginDescriptor.cs index ae9cc31b..21c92e4f 100644 --- a/ExternalPlugins/SppSecrets/PluginDescriptor.cs +++ b/ExternalPlugins/SppSecrets/PluginDescriptor.cs @@ -238,18 +238,26 @@ private bool StoreCredential(string assetName, string accountName, string payloa } } + var secretSaved = false; switch (AssignedCredentialType) { case CredentialType.Password: - SaveAccountPassword(account, payload); + secretSaved = SaveAccountPassword(account, payload); break; case CredentialType.SshKey: - SaveAccountSshKey(account, payload); + secretSaved = SaveAccountSshKey(account, payload); break; case CredentialType.ApiKey: - SaveAccountApiKey(account, payload); + secretSaved = SaveAccountApiKey(account, payload); break; } + + if (!secretSaved) + { + // No need to log here as the specific save method already logged the error. + return false; + } + Logger.Information($"The secret for {assetName}-{accountName} has been successfully stored in the vault."); // Look up A2A Registration and create one if needed. @@ -273,52 +281,56 @@ private bool StoreCredential(string assetName, string accountName, string payloa } } - private void SaveAccountPassword(Account account, string password) + private bool SaveAccountPassword(Account account, string password) { if (_sppConnection == null) - return; + return false; try { var result = _sppConnection.InvokeMethodFull(Service.Core, Method.Put, $"AssetAccounts/{account.Id}/Password", $"\"{password}\""); - if (result.StatusCode != HttpStatusCode.NoContent) + if (result.StatusCode == HttpStatusCode.NoContent) { - Logger.Error( - $"Failed to save the password for asset {account.Asset.Name} account {account.Name}"); + return true; } + + Logger.Error($"Failed to save the password for asset {account.Asset.Name} account {account.Name}"); } catch (Exception ex) { - Logger.Error(ex, - $"Failed to save the password for asset {account.Asset.Name} account {account.Name}: {ex.Message}"); + Logger.Error(ex, $"Failed to save the password for asset {account.Asset.Name} account {account.Name}: {ex.Message}"); } + + return false; } - private void SaveAccountSshKey(Account account, string sshKey) + private bool SaveAccountSshKey(Account account, string sshKey) { if (_sppConnection == null) - return; + return false; try { var result = _sppConnection.InvokeMethodFull(Service.Core, Method.Put, $"AssetAccounts/{account.Id}/SshKey", $"{{\"PrivateKey\":\"{sshKey.ReplaceLineEndings(string.Empty)}\"}}"); - if (result.StatusCode != HttpStatusCode.OK) + if (result.StatusCode == HttpStatusCode.OK) { - Logger.Error( - $"Failed to save the SSH key for asset {account.Asset.Name} account {account.Name}"); + return true; } + + Logger.Error($"Failed to save the SSH key for asset {account.Asset.Name} account {account.Name}"); } catch (Exception ex) { - Logger.Error(ex, - $"Failed to save the SSH key for asset {account.Asset.Name} account {account.Name}: {ex.Message}"); + Logger.Error(ex, $"Failed to save the SSH key for asset {account.Asset.Name} account {account.Name}: {ex.Message}"); } + + return false; } - private void SaveAccountApiKey(Account account, string apiKeyJson) + private bool SaveAccountApiKey(Account account, string apiKeyJson) { if (_sppConnection == null) - return; + return false; try { @@ -332,29 +344,31 @@ private void SaveAccountApiKey(Account account, string apiKeyJson) if (apiKey == null) { Logger.Information($"Failed to store the API key secret due to a failure to create the API key for the account {account.Name}."); - return; + return false; } } var result = _sppConnection.InvokeMethodFull(Service.Core, Method.Put, $"AssetAccounts/{account.Id}/ApiKeys/{newApiKey.Id}/ClientSecret", apiKeyJson); - if (result.StatusCode != HttpStatusCode.NoContent) + if (result.StatusCode == HttpStatusCode.NoContent) { - Logger.Error( - $"Failed to save the API key secret for account {account.Name} API key {apiKey.Name} to {DisplayName}."); + return true; } + + Logger.Error($"Failed to save the API key secret for account {account.Name} API key {apiKey.Name} to {DisplayName}."); } else { Logger.Error($"The ApiKey {apiKey.Name} failed to save to {DisplayName}."); } - } catch (Exception ex) { Logger.Error(ex, $"Failed to save the Api key for account {account.Name} to {DisplayName}: {ex.Message}"); } + + return false; } private bool CheckOrAddExternalA2aRegistration() diff --git a/ExternalPlugins/SppSecrets/README.md b/ExternalPlugins/SppSecrets/README.md index dd2ef300..79886569 100644 --- a/ExternalPlugins/SppSecrets/README.md +++ b/ExternalPlugins/SppSecrets/README.md @@ -7,7 +7,7 @@ The Safeguard for Privileged Passwords plugin allows Secrets Broker to pull pass ### Downstream SPP configuration requirements * Create a local SPP user that will be used by Secrets Broker to manage the A2A registration and accounts group in the downstream SPP appliance. - * Assign policy admin permissions to the new SPP user. + * Assign asset admin and policy admin permissions to the new SPP user. * This user name will be entered as the ```SPP user``` in the configuration of the SPPtoSPP Secrets Broker plugin. * Create an A2A certificate with ```Client Authentication``` attribute. * Install the A2A certificate with private key .pfx in the ```Local Computer``` certificate store of the Secrets Broker server. @@ -29,4 +29,4 @@ The Safeguard for Privileged Passwords plugin allows Secrets Broker to pull pass * **SPP User** - Downstream user name of an SPP local user with policy admin permissions. * **SPP A2A Registration Name** - Name of the downstream A2A registration that is used to provide A2A access to the credentials that are pushed by Secrets Broker. * **SPP A2a Certificate User** - Downstream A2A certificate user. -* **SPP Account Group** - Name of an account group for the credentials that are pushed by Secrets Broker. +* **SPP Account Group** - (Optional) Name of an account group for the credentials that are pushed by Secrets Broker. diff --git a/SafeguardDevOpsService/ClientApp/angular.json b/SafeguardDevOpsService/ClientApp/angular.json index 89e45d98..2079799a 100644 --- a/SafeguardDevOpsService/ClientApp/angular.json +++ b/SafeguardDevOpsService/ClientApp/angular.json @@ -18,6 +18,7 @@ "builder": "@angular-devkit/build-angular:browser", "options": { "allowedCommonJsDependencies": [ + "file-saver", "moment-timezone", "jquery", "lodash" diff --git a/SafeguardDevOpsService/ClientApp/package.json b/SafeguardDevOpsService/ClientApp/package.json index 7fcc597a..5a49814b 100644 --- a/SafeguardDevOpsService/ClientApp/package.json +++ b/SafeguardDevOpsService/ClientApp/package.json @@ -5,6 +5,8 @@ "ng": "ng", "start": "ng serve", "build": "ng build", + "build-debug-windows": "ng build --optimization=false && npm run copy-dist-to-debug-windows", + "copy-dist-to-debug-windows": "xcopy dist\\* ..\\bin\\Debug\\ClientApp\\dist\\ /y /E", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e" diff --git a/SafeguardDevOpsService/ClientApp/src/app/confirm-dialog/confirm-dialog.component.html b/SafeguardDevOpsService/ClientApp/src/app/confirm-dialog/confirm-dialog.component.html index 935b983d..e8fc83f5 100644 --- a/SafeguardDevOpsService/ClientApp/src/app/confirm-dialog/confirm-dialog.component.html +++ b/SafeguardDevOpsService/ClientApp/src/app/confirm-dialog/confirm-dialog.component.html @@ -37,6 +37,7 @@
- - + + +
\ No newline at end of file diff --git a/SafeguardDevOpsService/ClientApp/src/app/confirm-dialog/confirm-dialog.component.ts b/SafeguardDevOpsService/ClientApp/src/app/confirm-dialog/confirm-dialog.component.ts index f3cc7af1..54440e28 100644 --- a/SafeguardDevOpsService/ClientApp/src/app/confirm-dialog/confirm-dialog.component.ts +++ b/SafeguardDevOpsService/ClientApp/src/app/confirm-dialog/confirm-dialog.component.ts @@ -11,6 +11,7 @@ export class ConfirmDialogComponent implements OnInit { title = ''; message = ''; confirmText = 'OK'; + customText = ''; showCancel = true; showNo = false; showRestart = false; @@ -29,6 +30,7 @@ export class ConfirmDialogComponent implements OnInit { this.title = this.data.title ?? this.title; this.message = this.data.message ?? this.message; this.confirmText = this.data.confirmText ?? ((this.title.length > 0) ? this.title : this.confirmText); + this.customText = this.data.customText ?? this.customText; this.showCancel = this.data.showCancel ?? this.showCancel; this.showNo = this.data.showNo ?? this.showNo; this.showRestart = this.data.showRestart ?? this.showRestart; @@ -40,7 +42,7 @@ export class ConfirmDialogComponent implements OnInit { this.dialogRef.close(); } - confirm(confirm: boolean): void { - this.dialogRef.close({ result: confirm ? 'OK' : 'No', restart: this.restart, secretsBrokerOnly: this.secretsBrokerOnly, passphrase: this.passphrase }); + confirm(value: string): void { + this.dialogRef.close({ result: value, restart: this.restart, secretsBrokerOnly: this.secretsBrokerOnly, passphrase: this.passphrase }); } } diff --git a/SafeguardDevOpsService/ClientApp/src/app/edit-trusted-certificates/edit-trusted-certificates.component.html b/SafeguardDevOpsService/ClientApp/src/app/edit-trusted-certificates/edit-trusted-certificates.component.html index 815f9843..9305bdd9 100644 --- a/SafeguardDevOpsService/ClientApp/src/app/edit-trusted-certificates/edit-trusted-certificates.component.html +++ b/SafeguardDevOpsService/ClientApp/src/app/edit-trusted-certificates/edit-trusted-certificates.component.html @@ -1,6 +1,6 @@
- +
@@ -11,7 +11,7 @@ info
-
@@ -21,11 +21,11 @@
- +
- +
- Verify TLS Certifcate
@@ -37,11 +37,13 @@ Establishing a trusted connection requires that trusted certificates be added to the service. - - - {{cert.Subject}} - - + + + + {{cert.Subject}} + + + diff --git a/SafeguardDevOpsService/ClientApp/src/app/edit-trusted-certificates/edit-trusted-certificates.component.scss b/SafeguardDevOpsService/ClientApp/src/app/edit-trusted-certificates/edit-trusted-certificates.component.scss index 20c19e71..a867db8b 100644 --- a/SafeguardDevOpsService/ClientApp/src/app/edit-trusted-certificates/edit-trusted-certificates.component.scss +++ b/SafeguardDevOpsService/ClientApp/src/app/edit-trusted-certificates/edit-trusted-certificates.component.scss @@ -34,10 +34,6 @@ min-width:1px; } -.cert-list { - min-height: 300px; -} - :host ::ng-deep .mdc-list-item__end { display: none; } @@ -53,10 +49,11 @@ mat-list-option:last-of-type { .root-container { position: relative; overflow: hidden; + min-height: 300px; } mat-spinner { - margin: 60px auto; + margin: 0px auto; } .cert-details-container { @@ -80,6 +77,7 @@ mat-spinner { .cert-details-body { margin: 20px; background-color: $iris-pastel; + height: 100%; .cert-details-title { font-weight: 600; diff --git a/SafeguardDevOpsService/ClientApp/src/app/edit-trusted-certificates/edit-trusted-certificates.component.ts b/SafeguardDevOpsService/ClientApp/src/app/edit-trusted-certificates/edit-trusted-certificates.component.ts index f468d9a3..70915ab2 100644 --- a/SafeguardDevOpsService/ClientApp/src/app/edit-trusted-certificates/edit-trusted-certificates.component.ts +++ b/SafeguardDevOpsService/ClientApp/src/app/edit-trusted-certificates/edit-trusted-certificates.component.ts @@ -1,5 +1,5 @@ -import { Component, OnInit, Inject, ViewChild, AfterViewInit, ElementRef } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog'; +import { Component, OnInit, Inject, ViewChild, ElementRef } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { DevOpsServiceClient } from '../service-client.service'; import { MatSelectionList } from '@angular/material/list'; import * as moment from 'moment-timezone'; @@ -13,15 +13,16 @@ import { MatSnackBar } from '@angular/material/snack-bar'; templateUrl: './edit-trusted-certificates.component.html', styleUrls: ['./edit-trusted-certificates.component.scss'] }) -export class EditTrustedCertificatesComponent implements OnInit, AfterViewInit { +export class EditTrustedCertificatesComponent implements OnInit { trustedCertificates: any[]; useSsl: boolean; selectedCert: any; localizedValidFrom: string; - isLoading: boolean; + isLoading: boolean = true; showExplanatoryText: boolean; error = null; + needsReload = false; @ViewChild('certificates', { static: false }) certList: MatSelectionList; @ViewChild('fileSelectInputDialog', { static: false }) fileSelectInputDialog: ElementRef; @@ -30,26 +31,30 @@ export class EditTrustedCertificatesComponent implements OnInit, AfterViewInit { @Inject(MAT_DIALOG_DATA) public data: any, private serviceClient: DevOpsServiceClient, private dialog: MatDialog, + private dialogRef: MatDialogRef, private snackbar: MatSnackBar ) { } ngOnInit(): void { this.trustedCertificates = this.data?.trustedCertificates ?? []; - this.serviceClient.getSafeguard().subscribe((data: any) => { + this.serviceClient.getSafeguard(false).subscribe((data: any) => { if (data) { this.useSsl = !data.IgnoreSsl; } + this.isLoading = false; }); + + this.dialogRef.backdropClick().subscribe(() => this.close()); } - - ngAfterViewInit(): void { - this.certList.selectionChange.subscribe((x) => { - if (!this.selectedCert) { - this.selectedCert = x.options[0].value; - this.localizedValidFrom = moment(this.selectedCert.NotBefore).format('LLL (Z)') + ' - ' + moment(this.selectedCert.NotAfter).format('LLL (Z)'); - } - }); + + close() { + this.dialogRef.close(this.needsReload); + } + + selectCert(cert) { + this.selectedCert = cert; + this.localizedValidFrom = moment(this.selectedCert.NotBefore).format('LLL (Z)') + ' - ' + moment(this.selectedCert.NotAfter).format('LLL (Z)'); } browse(): void { @@ -59,6 +64,7 @@ export class EditTrustedCertificatesComponent implements OnInit, AfterViewInit { updateUseSsl(): void { this.error = null; + this.needsReload = true; this.serviceClient.putSafeguardUseSsl(this.useSsl) .subscribe({ next: () => { }, @@ -121,6 +127,7 @@ export class EditTrustedCertificatesComponent implements OnInit, AfterViewInit { next: () => { if (isNew) { this.snackbar.open(`Added certificate ${fileData.fileName}`, 'Dismiss', { duration: 5000 }); + this.needsReload = true; } else { this.snackbar.open(`Certificate ${fileData.fileName} already exists.`, 'Dismiss', { duration: 5000 }); } @@ -197,6 +204,10 @@ export class EditTrustedCertificatesComponent implements OnInit, AfterViewInit { } else { this.snackbar.open(`Imported ${newTrustedCertsCount} new certificates and ${existingTrustedCertsCount} existing certificates.`, 'Dismiss', { duration: 5000 }); } + + if (newTrustedCertsCount > 0) { + this.needsReload = true; + } } }); } @@ -215,6 +226,7 @@ export class EditTrustedCertificatesComponent implements OnInit, AfterViewInit { this.updateUseSsl(); } this.isLoading = false; + this.needsReload = true; }, error: error => { this.isLoading = false; diff --git a/SafeguardDevOpsService/ClientApp/src/app/main/main.component.html b/SafeguardDevOpsService/ClientApp/src/app/main/main.component.html index 8c4035b6..6432da00 100644 --- a/SafeguardDevOpsService/ClientApp/src/app/main/main.component.html +++ b/SafeguardDevOpsService/ClientApp/src/app/main/main.component.html @@ -65,7 +65,7 @@

- Monitoring is unavailable. + Monitoring is unavailable.

error_outline @@ -80,12 +80,22 @@
+ [disabled]='isUploading.Plugin || isUploading.Addon || isUploading.Backup || isRestarting || isMonitoringStarting || isMonitoringStopping'> + Start Monitoring + + + Starting Monitoring + + + [disabled]='isUploading.Plugin || isUploading.Addon || isUploading.Backup || isRestarting || isMonitoringStarting || isMonitoringStopping'> + Stop Monitoring + + + Stopping Monitoring + +
diff --git a/SafeguardDevOpsService/ClientApp/src/app/main/main.component.scss b/SafeguardDevOpsService/ClientApp/src/app/main/main.component.scss index 69bd950c..3bfb45ed 100644 --- a/SafeguardDevOpsService/ClientApp/src/app/main/main.component.scss +++ b/SafeguardDevOpsService/ClientApp/src/app/main/main.component.scss @@ -48,7 +48,7 @@ mat-drawer-container { .footer { color: $black-9; text-align: right; - padding-right: 15px; + padding-right: 50px; padding-bottom: 15px; } @@ -123,13 +123,19 @@ mat-drawer-container { color: #ffffff; margin-top: 20px; width: 100%; + + mat-spinner { + display: inline-block; + vertical-align: text-bottom; + margin-inline-end: 5px; + } } - .start-monitoring { + .start-monitoring:not(:disabled) { background-color: $aspen-green; } - .stop-monitoring { + .stop-monitoring:not(:disabled) { background-color: $phoenix-red; } diff --git a/SafeguardDevOpsService/ClientApp/src/app/main/main.component.ts b/SafeguardDevOpsService/ClientApp/src/app/main/main.component.ts index adce08ca..6969ee6e 100644 --- a/SafeguardDevOpsService/ClientApp/src/app/main/main.component.ts +++ b/SafeguardDevOpsService/ClientApp/src/app/main/main.component.ts @@ -65,6 +65,8 @@ export class MainComponent implements OnInit, AfterViewInit { trustedCertificates = []; isMonitoring: boolean; isMonitoringAvailable: boolean; + isMonitoringStarting: boolean = false; + isMonitoringStopping: boolean = false; unconfiguredDiv: any; footerAbsolute: boolean = true; restartingProgress: string = 'Restarting Service'; @@ -575,7 +577,7 @@ export class MainComponent implements OnInit, AfterViewInit { } editAddon(addon: any): void { - if (this.isUploading.Plugin || this.isUploading.Addon || this.isRestarting) { + if (this.isUploading.Plugin || this.isUploading.Addon || this.isRestarting || this.isMonitoringStarting || this.isMonitoringStopping) { return; } @@ -612,7 +614,7 @@ export class MainComponent implements OnInit, AfterViewInit { } editPlugin(plugin: any): void { - if (this.isUploading.Plugin || this.isUploading.Addon || this.isRestarting || !plugin.IsLoaded) { + if (this.isUploading.Plugin || this.isUploading.Addon || this.isRestarting || !plugin.IsLoaded || this.isMonitoringStarting || this.isMonitoringStopping) { return; } @@ -656,18 +658,17 @@ export class MainComponent implements OnInit, AfterViewInit { const dialogRef = this.dialog.open(ConfirmDialogComponent, { data: { title: 'Plugin Configuration Changed', - message: 'Restart the monitor to apply the new plugin configuration.', + message: 'You will need to restart the monitor to apply the new plugin configuration.', showCancel: false, - confirmText: 'OK' + customText: 'Later', + confirmText: 'Restart Monitoring' } }); - dialogRef.afterClosed().pipe( - filter((dlgResult) => dlgResult?.result === 'OK'), - ).subscribe({ - next: () => { - if (data.reload === true) { - this.window.location.reload(); + dialogRef.afterClosed().subscribe({ + next: (dlgResult: any) => { + if (dlgResult?.result === 'OK') { + this.restartMonitoring(); } } }); @@ -787,23 +788,57 @@ export class MainComponent implements OnInit, AfterViewInit { updateMonitoring(enabled: boolean): void { this.error = null; + this.isMonitoringStarting = enabled; + this.isMonitoringStopping = !enabled; this.serviceClient.postMonitor(enabled).pipe( + switchMap(_ => this.initializePlugins()), untilDestroyed(this) ).subscribe({ next: () => { this.isMonitoring = enabled; - this.updateMonitoringAvailable(); - setTimeout(() => { - this.setArrows(); - }, 100); + this.updateMonitoringStatus(); }, error: error => { this.error = error; + this.isMonitoringStarting = false; + this.isMonitoringStopping = false; } }); + } + restartMonitoring(): void { + this.error = null; + this.isMonitoringStopping = true; + this.serviceClient.postMonitor(false).pipe( + switchMap(_ => { + this.isMonitoringStopping = false; + this.isMonitoring = false; + this.isMonitoringStarting = true; + return this.serviceClient.postMonitor(true); + }), + switchMap(_ => this.initializePlugins()), + untilDestroyed(this) + ).subscribe({ + next: () => { + this.isMonitoring = true; + this.updateMonitoringStatus(); + + this.snackBar.open("Monitoring restarted", 'Dismiss', { duration: 10000 }); + }, + error: error => { + this.error = error; + this.isMonitoringStarting = false; + this.isMonitoringStopping = false; + } + }); + } + + updateMonitoringStatus(): void { setTimeout(() => { - if (enabled === true) { + this.updateMonitoringAvailable(); + this.setArrows(); + + if (this.isMonitoring === true) { this.serviceClient.getMonitor().pipe( untilDestroyed(this) ).subscribe({ @@ -811,13 +846,21 @@ export class MainComponent implements OnInit, AfterViewInit { if (status.StatusMessage != null) { this.snackBar.open(status.StatusMessage, 'Dismiss', { duration: 10000 }); } + this.isMonitoringStarting = false; + this.isMonitoringStopping = false; }, error: error => { - this.snackBar.open('Failed to get the monitor status: ' + this.error, 'Dismiss', { duration: 10000 }); + this.snackBar.open('Failed to get the monitor status: ' + error, 'Dismiss', { duration: 10000 }); + this.isMonitoringStarting = false; + this.isMonitoringStopping = false; } }); } - }, 3000); + else { + this.isMonitoringStarting = false; + this.isMonitoringStopping = false; + } + }, 100); } logout(): void { @@ -1070,12 +1113,15 @@ export class MainComponent implements OnInit, AfterViewInit { const dialogRef = this.dialog.open(EditTrustedCertificatesComponent, { width: '500px', + disableClose: true, data: { trustedCertificates: this.trustedCertificates } }); dialogRef.afterClosed().subscribe({ - next: () => { - this.window.location.reload(); + next: (needsReload: boolean) => { + if (needsReload) { + this.window.location.reload(); + } } }); } diff --git a/SafeguardDevOpsService/ClientApp/src/app/service-client.service.ts b/SafeguardDevOpsService/ClientApp/src/app/service-client.service.ts index 125c1017..7a7d7b60 100644 --- a/SafeguardDevOpsService/ClientApp/src/app/service-client.service.ts +++ b/SafeguardDevOpsService/ClientApp/src/app/service-client.service.ts @@ -64,8 +64,8 @@ export class DevOpsServiceClient { return query; } - getSafeguard(): Observable { - const url = this.BASE + 'Safeguard'; + getSafeguard(includeDetails: boolean = true): Observable { + const url = this.BASE + 'Safeguard?includeDetails=' + includeDetails; return this.http.get(url) .pipe( diff --git a/SafeguardDevOpsService/ClientApp/src/styles.scss b/SafeguardDevOpsService/ClientApp/src/styles.scss index 664037d7..0cca60ae 100644 --- a/SafeguardDevOpsService/ClientApp/src/styles.scss +++ b/SafeguardDevOpsService/ClientApp/src/styles.scss @@ -71,10 +71,28 @@ body { --mdc-filled-button-label-text-color: #ffff; } +.mat-mdc-dialog-container .mdc-dialog__title { + --mdc-dialog-subhead-line-height: 14px; +} +.mdc-dialog__title::before{ + height: unset !important; +} .mdc-dialog__actions { padding: 20px !important; } +.mat-mdc-text-field-wrapper.mdc-text-field--outlined .mat-mdc-form-field-infix { + padding: 7px 0px; + min-height: 36px; +} +.mat-mdc-text-field-wrapper .mat-mdc-form-field-flex .mat-mdc-floating-label { + top: 18px; + + &.mdc-floating-label--float-above { + top: 28px; + } +} + // Global styles for modal dialog type header .dialog-title-elements { display: flex; diff --git a/SafeguardDevOpsService/Controllers/V2/SafeguardController.cs b/SafeguardDevOpsService/Controllers/V2/SafeguardController.cs index 58f4ac15..93e7fd15 100644 --- a/SafeguardDevOpsService/Controllers/V2/SafeguardController.cs +++ b/SafeguardDevOpsService/Controllers/V2/SafeguardController.cs @@ -45,9 +45,9 @@ public SafeguardController() /// Success. [UnhandledExceptionError] [HttpGet] - public ActionResult GetSafeguard([FromServices] ISafeguardLogic safeguard) + public ActionResult GetSafeguard([FromServices] ISafeguardLogic safeguard, [FromQuery] bool includeDetails = true) { - var safeguardConnection = safeguard.GetAnonymousSafeguardConnection(); + var safeguardConnection = safeguard.GetAnonymousSafeguardConnection(includeDetails); return Ok(safeguardConnection); } diff --git a/SafeguardDevOpsService/Extensions/CertificateExtensions.cs b/SafeguardDevOpsService/Extensions/CertificateExtensions.cs index 8ee98d41..9b1cdc39 100644 --- a/SafeguardDevOpsService/Extensions/CertificateExtensions.cs +++ b/SafeguardDevOpsService/Extensions/CertificateExtensions.cs @@ -4,8 +4,18 @@ namespace OneIdentity.DevOps.Extensions { + /// + /// Provides utility methods for working with certificates. + /// public static class CertificateExtensions { + /// + /// Loads an X509Certificate2 from a byte array, handling both PKCS12 (PFX) and PEM formats. + /// + /// The certificate byte array. + /// The password used to load the certificate data, if needed. + /// Flags determining how the key is handled. + /// public static X509Certificate2 LoadFromBytes(byte[] rawData, string? password = null, X509KeyStorageFlags? keyStorageFlags = null) { // 1. Detect if the byte array is PKCS12 or PEM diff --git a/SafeguardDevOpsService/Logic/ISafeguardLogic.cs b/SafeguardDevOpsService/Logic/ISafeguardLogic.cs index 6efc7856..0e93a1aa 100644 --- a/SafeguardDevOpsService/Logic/ISafeguardLogic.cs +++ b/SafeguardDevOpsService/Logic/ISafeguardLogic.cs @@ -16,7 +16,7 @@ public interface ISafeguardLogic DevOpsSecretsBroker DevOpsSecretsBrokerCache { get; } ISafeguardConnection Connect(); - SafeguardDevOpsConnection GetAnonymousSafeguardConnection(); + SafeguardDevOpsConnection GetAnonymousSafeguardConnection(bool includeDetails); SafeguardDevOpsConnection GetSafeguardConnection(); SafeguardDevOpsLogon GetSafeguardLogon(); SafeguardDevOpsConnection SetSafeguardData(string token, SafeguardData safeguardData); diff --git a/SafeguardDevOpsService/Logic/SafeguardLogic.cs b/SafeguardDevOpsService/Logic/SafeguardLogic.cs index db1b76b3..191914dc 100644 --- a/SafeguardDevOpsService/Logic/SafeguardLogic.cs +++ b/SafeguardDevOpsService/Logic/SafeguardLogic.cs @@ -23,6 +23,9 @@ using OneIdentity.DevOps.Extensions; using A2ARetrievableAccount = OneIdentity.DevOps.Data.Spp.A2ARetrievableAccount; using Microsoft.AspNetCore.Http; +using Polly; +using Polly.Retry; + // ReSharper disable InconsistentNaming namespace OneIdentity.DevOps.Logic @@ -43,6 +46,7 @@ internal class SafeguardLogic : ISafeguardLogic, IDisposable private ServiceConfiguration _serviceConfiguration => AuthorizedCache.Instance.FindByToken(_threadToken); private DevOpsSecretsBroker _devOpsSecretsBrokerCache; + private readonly ResiliencePipeline pollyPipeline; public DevOpsSecretsBroker DevOpsSecretsBrokerCache { @@ -66,6 +70,9 @@ public SafeguardLogic(IConfigurationRepository configDb, Func plu _addonLogic = addonLogic; _addonManager = addonManager; _logger = Serilog.Log.Logger; + var options = new RetryStrategyOptions(); + options.MaxRetryAttempts = 1; + pollyPipeline = new ResiliencePipelineBuilder().AddRetry(options).Build(); } bool CertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, @@ -262,17 +269,20 @@ private SafeguardDevOpsConnection GetSafeguardAppliance(ISafeguardConnection sgC private SafeguardDevOpsConnection GetSafeguardAvailability(ISafeguardConnection sgConnection, SafeguardDevOpsConnection safeguardConnection) { - var safeguard = GetSafeguardAppliance(sgConnection, safeguardConnection.ApplianceAddress); - safeguardConnection.ApplianceId = safeguard.ApplianceId; - safeguardConnection.ApplianceName = safeguard.ApplianceName; - safeguardConnection.ApplianceVersion = safeguard.ApplianceVersion; - safeguardConnection.ApplianceState = safeguard.ApplianceState; - safeguardConnection.ApplianceSupportsDevOps = safeguard.ApplianceSupportsDevOps; - safeguardConnection.DevOpsInstanceId = _configDb.SvcId; - safeguardConnection.UserName = safeguard.UserName; - safeguardConnection.UserDisplayName = safeguard.UserDisplayName; - safeguardConnection.AdminRoles = safeguard.AdminRoles; - safeguardConnection.Version = safeguard.Version; + pollyPipeline.Execute(_ => + { + var safeguard = GetSafeguardAppliance(sgConnection, safeguardConnection.ApplianceAddress); + safeguardConnection.ApplianceId = safeguard.ApplianceId; + safeguardConnection.ApplianceName = safeguard.ApplianceName; + safeguardConnection.ApplianceVersion = safeguard.ApplianceVersion; + safeguardConnection.ApplianceState = safeguard.ApplianceState; + safeguardConnection.ApplianceSupportsDevOps = safeguard.ApplianceSupportsDevOps; + safeguardConnection.DevOpsInstanceId = _configDb.SvcId; + safeguardConnection.UserName = safeguard.UserName; + safeguardConnection.UserDisplayName = safeguard.UserDisplayName; + safeguardConnection.AdminRoles = safeguard.AdminRoles; + safeguardConnection.Version = safeguard.Version; + }); return safeguardConnection; } @@ -1871,7 +1881,7 @@ public DevOpsSecretsBroker ConfigureDevOpsService() return GetDevOpsConfiguration(sg); } - public SafeguardDevOpsConnection GetAnonymousSafeguardConnection() + public SafeguardDevOpsConnection GetAnonymousSafeguardConnection(bool includeDetails) { if (string.IsNullOrEmpty(_configDb.SafeguardAddress)) return new SafeguardDevOpsConnection(); @@ -1886,6 +1896,11 @@ public SafeguardDevOpsConnection GetAnonymousSafeguardConnection() ApiVersion = _configDb.ApiVersion ?? WellKnownData.DefaultApiVersion }; + if (!includeDetails) + { + return safeguardConnection; + } + sg = Safeguard.Connect(safeguardConnection.ApplianceAddress, safeguardConnection.ApiVersion ?? WellKnownData.DefaultApiVersion, true); return GetSafeguardAvailability(sg, safeguardConnection); diff --git a/SafeguardDevOpsService/Program.cs b/SafeguardDevOpsService/Program.cs index 8a3b04fa..17f985f8 100644 --- a/SafeguardDevOpsService/Program.cs +++ b/SafeguardDevOpsService/Program.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Runtime.InteropServices; +using Microsoft.Extensions.Configuration; using OneIdentity.DevOps.Logic; using Serilog; using Topshelf; @@ -15,21 +16,48 @@ internal class Program private static void Main() { - // Before doing anything, check of there is a staged restore. + // Before doing anything, check if there is a staged restore. RestoreManager.CheckForStagedRestore(); Directory.CreateDirectory(WellKnownData.ProgramDataPath); var logDirPath = WellKnownData.LogDirPath; + var configuration = new ConfigurationBuilder() + .AddJsonFile(WellKnownData.AppSettingsFile, optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .Build(); + + var rollingInterval = RollingInterval.Day; + if (Enum.TryParse(configuration["LogRollingInterval"] ?? "Day", out RollingInterval interval)) + { + rollingInterval = interval; + } + + int? logFileCountLimit = 31; + if (int.TryParse(configuration["LogFileCountLimit"], out int limit)) + { + logFileCountLimit = limit == 0 ? null : limit; + } + Log.Logger = new LoggerConfiguration() .WriteTo.File(logDirPath, shared: true, - outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u1}] {Message:lj}{NewLine}{Exception}") + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u1}] {Message:lj}{NewLine}{Exception}", + rollingInterval: rollingInterval, + retainedFileCountLimit: logFileCountLimit) .Enrich.FromLogContext() .Enrich.WithThreadId() .MinimumLevel.ControlledBy(LogLevelSwitcher.Instance.LogLevelSwitch) .CreateLogger(); Console.WriteLine($"Safeguard Secrets Broker for DevOps logging to: {logDirPath}"); + if (rollingInterval != RollingInterval.Infinite) + { + Console.WriteLine($" - Logs will roll every {rollingInterval}."); + if (logFileCountLimit.HasValue) + { + Console.WriteLine($" - Only the {logFileCountLimit} most recent log files, including the current one, will be retained."); + } + } RestartManager.Instance.ShouldRestart = false; HostFactory.Run(hostConfig => diff --git a/SafeguardDevOpsService/SafeguardDevOpsService.csproj b/SafeguardDevOpsService/SafeguardDevOpsService.csproj index b9249020..a92e8775 100644 --- a/SafeguardDevOpsService/SafeguardDevOpsService.csproj +++ b/SafeguardDevOpsService/SafeguardDevOpsService.csproj @@ -59,6 +59,7 @@ + diff --git a/SafeguardDevOpsService/_appsettings.json b/SafeguardDevOpsService/_appsettings.json index 652a2d0c..7bcd2714 100644 --- a/SafeguardDevOpsService/_appsettings.json +++ b/SafeguardDevOpsService/_appsettings.json @@ -1,6 +1,8 @@ { - // Rename to appsetttings.json (remove the leading underscore), then restart the service. + // Rename to appsettings.json (remove the leading underscore), then restart the service. // You can change the LogLevel to Debug for more logging. "HttpsPort": "443", - "LogLevel": "Information" + "LogLevel": "Information", + "LogRollingInterval": "Day", // Infinite, Minute, Hour, Day, Month, Year + "LogFileCountLimit": "31" // Use '0' for no limit } \ No newline at end of file