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 @@
Cancel
- No
- {{confirmText}}
+ No
+ {{customText}}
+ {{confirmText}}
\ 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 @@
- Upload
+ Upload
-
Import from Safeguard
+
Import from Safeguard
-
Verify TLS Certifcate
@@ -37,11 +37,13 @@
Establishing a trusted connection requires that trusted certificates be added to the service.
- 0}">
-
- {{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 @@
Start
- Monitoring
+ [disabled]='isUploading.Plugin || isUploading.Addon || isUploading.Backup || isRestarting || isMonitoringStarting || isMonitoringStopping'>
+
Start Monitoring
+
+
+ Starting Monitoring
+
+
Stop
- 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