Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/ginger analytics config page b #3895

Merged
merged 14 commits into from
Sep 16, 2024
1 change: 1 addition & 0 deletions Ginger/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ csharp_style_prefer_extended_property_pattern = true:suggestion
csharp_style_var_for_built_in_types = true:none
csharp_style_var_when_type_is_apparent = true:none
csharp_style_var_elsewhere = true:none
csharp_indent_braces = false

[*.vb]
#### Naming styles ####
Expand Down
120 changes: 120 additions & 0 deletions Ginger/Ginger/ExternalConfigurations/GingerAnalyticsAPI.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#region License
/*
Copyright © 2014-2024 European Support Limited

Licensed under the Apache License, Version 2.0 (the "License")
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion

using Ginger.Configurations;
using Amdocs.Ginger.Common;
using amdocs.ginger.GingerCoreNET;
using System.Net.Http;
using System.Threading.Tasks;
using System;
using IdentityModel.Client;
using System.IdentityModel.Tokens.Jwt;

namespace Ginger.ExternalConfigurations
{
public class GingerAnalyticsAPI

Check warning on line 30 in Ginger/Ginger/ExternalConfigurations/GingerAnalyticsAPI.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Ginger/Ginger/ExternalConfigurations/GingerAnalyticsAPI.cs#L30

Add a 'protected' constructor or the 'static' keyword to the class declaration.
{
public static DateTime validTo = DateTime.MinValue;

Check failure on line 32 in Ginger/Ginger/ExternalConfigurations/GingerAnalyticsAPI.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Ginger/Ginger/ExternalConfigurations/GingerAnalyticsAPI.cs#L32

Change the visibility of 'validTo' or make it 'const' or 'readonly'.
public static GingerAnalyticsConfiguration gingerAnalyticsUserConfig =

Check failure on line 33 in Ginger/Ginger/ExternalConfigurations/GingerAnalyticsAPI.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Ginger/Ginger/ExternalConfigurations/GingerAnalyticsAPI.cs#L33

Change the visibility of 'gingerAnalyticsUserConfig' or make it 'const' or 'readonly'.
WorkSpace.Instance.SolutionRepository.GetAllRepositoryItems<GingerAnalyticsConfiguration>().Count == 0 ? new GingerAnalyticsConfiguration() : WorkSpace.Instance.SolutionRepository.GetFirstRepositoryItem<GingerAnalyticsConfiguration>();
Comment on lines +64 to +66
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Address static analysis hints.

Codacy suggests changing the visibility of validTo and gingerAnalyticsUserConfig or making them const or readonly. While making them const or readonly is not desirable in this case, changing their visibility to private and providing thread-safe access methods could help ensure thread safety and encapsulation.

Consider the following changes:

  1. Change the visibility of validTo and gingerAnalyticsUserConfig to private.
  2. Provide thread-safe access methods for reading and updating these fields.
-public static DateTime validTo = DateTime.MinValue;
-public static GingerAnalyticsConfiguration gingerAnalyticsUserConfig =
-    WorkSpace.Instance.SolutionRepository.GetAllRepositoryItems<GingerAnalyticsConfiguration>().Count == 0 ? new GingerAnalyticsConfiguration() : WorkSpace.Instance.SolutionRepository.GetFirstRepositoryItem<GingerAnalyticsConfiguration>();
+private static DateTime validTo = DateTime.MinValue;
+private static GingerAnalyticsConfiguration gingerAnalyticsUserConfig =
+    WorkSpace.Instance.SolutionRepository.GetAllRepositoryItems<GingerAnalyticsConfiguration>().Count == 0 ? new GingerAnalyticsConfiguration() : WorkSpace.Instance.SolutionRepository.GetFirstRepositoryItem<GingerAnalyticsConfiguration>();
+private static readonly object configLock = new object();

+public static DateTime GetValidTo()
+{
+    lock (configLock)
+    {
+        return validTo;
+    }
+}

+public static void SetValidTo(DateTime value)
+{
+    lock (configLock)
+    {
+        validTo = value;
+    }
+}

+public static GingerAnalyticsConfiguration GetGingerAnalyticsUserConfig()
+{
+    lock (configLock)
+    {
+        return gingerAnalyticsUserConfig;
+    }
+}

+public static void SetGingerAnalyticsUserConfig(GingerAnalyticsConfiguration value)
+{
+    lock (configLock)
+    {
+        gingerAnalyticsUserConfig = value;
+    }
+}

Committable suggestion was skipped due to low confidence.

Tools
GitHub Check: Codacy Static Code Analysis

[failure] 64-64: Ginger/Ginger/ExternalConfigurations/GingerAnalyticsAPI.cs#L64
Change the visibility of 'validTo' or make it 'const' or 'readonly'.


[failure] 65-65: Ginger/Ginger/ExternalConfigurations/GingerAnalyticsAPI.cs#L65
Change the visibility of 'gingerAnalyticsUserConfig' or make it 'const' or 'readonly'.



public static async Task<bool> RequestToken(string clientId, string clientSecret, string address)
{
try
{
HttpClientHandler handler = new HttpClientHandler() { UseProxy = false };

using (var client = new HttpClient(handler))
{

var disco = await client.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest
{
Address = address,
Policy =
{
RequireHttps = true,
ValidateIssuerName = true
}
});

if (disco.IsError)
{
Reporter.ToLog(eLogLevel.ERROR, $"Discovery document error: {disco.Error}");
return false;
}

var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = clientId,
ClientSecret = clientSecret
});

if (tokenResponse.IsError)
{
Reporter.ToLog(eLogLevel.ERROR, $"Token request error: {tokenResponse.Error}");
return false;
}

validTo = DateTime.UtcNow.AddMinutes(60);
gingerAnalyticsUserConfig.Token = tokenResponse.AccessToken;
return true;
}
}
catch (HttpRequestException httpEx)
{
Reporter.ToLog(eLogLevel.ERROR, "HTTP request failed", httpEx);
return false;
}
catch (Exception ex)
{
Reporter.ToLog(eLogLevel.ERROR, "Unexpected error during token request", ex);
return false;
}
}

public static bool IsTokenValid()
{
try
{
if (string.IsNullOrEmpty(gingerAnalyticsUserConfig.Token) || gingerAnalyticsUserConfig.Token.Split('.').Length != 3)
{
return false;
}

var handler = new JwtSecurityTokenHandler();
var jwtToken = handler.ReadJwtToken(gingerAnalyticsUserConfig.Token);
validTo = jwtToken.ValidTo;
if (DateTime.UtcNow < validTo)
{
return true;
}
else
{
return false;
}
}
catch (Exception ex)
{
Reporter.ToLog(eLogLevel.ERROR, "Error occured in validate token", ex);
return false;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<UserControlsLib:GingerUIPage x:Class="Ginger.ExternalConfigurations.GingerAnalyticsConfigurationPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Ginger.ExternalConfigurations"
xmlns:usercontrols="clr-namespace:Amdocs.Ginger.UserControls"
xmlns:UserControlsLib="clr-namespace:Ginger.UserControlsLib"
xmlns:Activities="clr-namespace:Ginger.BusinessFlowWindows"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Title="GingerAnalyticsConfigurationPage">

<DockPanel Background="{StaticResource $BackgroundColor_White}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row="0">
<Label Content="Ginger Analytics Configuration" Style="{StaticResource $HorizontalExpanderLabelStyle}"/>
<usercontrols:ImageMakerControl SetAsFontImageWithSize="16" ToolTip="Enterprise Feature" ImageType="Building" Width="20" Height="16" Foreground="{StaticResource $BackgroundColor_Black}" />
</StackPanel>
<StackPanel Orientation="Vertical" Grid.Row="1">
<Grid Margin="10,10,0,0" x:Name="xAnalyticsGrid" >
<Grid.RowDefinitions>
<RowDefinition Height="50"/>
<RowDefinition Height="50"/>
<RowDefinition Height="50"/>
<RowDefinition Height="50"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="230"/>
<ColumnDefinition Width="350*"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Grid.Column="0" Grid.Row="0">
<Label x:Name="xGAAURLLabel" Content="Ginger Analytics Account URL:" Style="{StaticResource @InputFieldLabelStyle}" VerticalAlignment="Center" FontSize="12"/>
<Label x:Name="xGAAURLLabelValidation" Content="*" Style="{StaticResource @InputFieldLabelStyle}" VerticalAlignment="Center" Foreground="Red" FontWeight="Bold" FontSize="12"/>
</StackPanel>
<Activities:UCValueExpression x:Name="xGAAURLTextBox" Grid.Column="1" Grid.Row="0" HorizontalAlignment="Left" VerticalAlignment="Center" ToolTip="Ginger Analytics Account URL" Margin="10,0,0,0" Width="400"/>

<StackPanel Orientation="Horizontal" Grid.Column="0" Grid.Row="1">
<Label x:Name="xISURLLabel" Content="Identity Service URL:" Style="{StaticResource @InputFieldLabelStyle}" VerticalAlignment="Center" FontSize="12"/>
<Label x:Name="xISURLLabelValidation" Content="*" Style="{StaticResource @InputFieldLabelStyle}" VerticalAlignment="Center" Foreground="Red" FontWeight="Bold" FontSize="12"/>
</StackPanel>
<Activities:UCValueExpression x:Name="xISURLTextBox" Grid.Column="1" Grid.Row="1" HorizontalAlignment="Left" VerticalAlignment="Center" ToolTip="Identiry Service URL" Margin="10,0,0,0" Width="400"/>

<StackPanel Orientation="Horizontal" Grid.Column="0" Grid.Row="2">
<Label x:Name="xClientIdLabel" Content="Client Id:" Style="{StaticResource @InputFieldLabelStyle}" VerticalAlignment="Center" FontSize="12"/>
<Label x:Name="xClientIdLabelValidation" Content="*" Style="{StaticResource @InputFieldLabelStyle}" VerticalAlignment="Center" Foreground="Red" FontWeight="Bold" FontSize="12"/>
</StackPanel>
<Activities:UCValueExpression x:Name="xClientIdTextBox" Grid.Column="1" Grid.Row="2" HorizontalAlignment="Left" VerticalAlignment="Center" ToolTip="Client Id" Margin="10,0,0,0" Width="400" LostKeyboardFocus="xClientIdTextBox_LostKeyboardFocus"/>

<StackPanel Orientation="Horizontal" Grid.Column="0" Grid.Row="3">
<Label x:Name="xClientSecretLabel" Content="Client Secret:" Style="{StaticResource @InputFieldLabelStyle}" VerticalAlignment="Center" FontSize="12"/>
<Label x:Name="xClientSecretValidation" Content="*" Style="{StaticResource @InputFieldLabelStyle}" VerticalAlignment="Center" Foreground="Red" FontWeight="Bold" FontSize="12"/>
</StackPanel>
<Activities:UCValueExpression x:Name="xClientSecretTextBox" Grid.Column="1" Grid.Row="3" HorizontalAlignment="Left" VerticalAlignment="Center" ToolTip="Client Secret" Margin="10,0,0,0" Width="400" LostKeyboardFocus="xClientSecretTextBox_LostKeyboardFocus"/>
</Grid>
<Grid>
<Button x:Name="xTestConBtn" IsEnabled="True" Click="xTestConBtn_Click" Content="Test Connection" Width="100" Background="White" Style="{StaticResource $InputButtonStyle}" Margin="10,15,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" />
<usercontrols:ImageMakerControl x:Name="xProcessingImage" ImageType="Processing" Margin="120,10,0,0" HorizontalAlignment="Left" Height="30" Width="20" Visibility="Hidden"></usercontrols:ImageMakerControl>

</Grid>
</StackPanel>
</Grid>
</DockPanel>
</UserControlsLib:GingerUIPage>
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
#region License
/*
Copyright © 2014-2024 European Support Limited

Licensed under the Apache License, Version 2.0 (the "License")
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion

using Ginger.Configurations;
using Ginger.UserControlsLib;
using Amdocs.Ginger.Common;
using System.Windows;
using GingerCore;
using amdocs.ginger.GingerCoreNET;
using System.Net.Http;
using System.Threading.Tasks;
using System;
using IdentityModel.Client;
using Ginger.ValidationRules;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using Microsoft.IdentityModel.Tokens;
using System.Text;

namespace Ginger.ExternalConfigurations
{
/// <summary>
/// Interaction logic for GingerAnalyticsConfigurationPage.xaml
/// </summary>
public partial class GingerAnalyticsConfigurationPage : GingerUIPage
{
public GingerAnalyticsConfiguration gingerAnalyticsUserConfig;
public GingerAnalyticsAPI analyticsAPI;
public GingerAnalyticsConfigurationPage()
{
InitializeComponent();
Init();
}
private void Init()
{
analyticsAPI = new GingerAnalyticsAPI();
gingerAnalyticsUserConfig = WorkSpace.Instance.SolutionRepository.GetAllRepositoryItems<GingerAnalyticsConfiguration>().Count == 0 ? new GingerAnalyticsConfiguration() : WorkSpace.Instance.SolutionRepository.GetFirstRepositoryItem<GingerAnalyticsConfiguration>();
gingerAnalyticsUserConfig.StartDirtyTracking();
SetControls();

}

private void SetControls()
{
Context mContext = new();

xGAAURLTextBox.Init(mContext, gingerAnalyticsUserConfig, nameof(GingerAnalyticsConfiguration.AccountUrl));
xISURLTextBox.Init(mContext, gingerAnalyticsUserConfig, nameof(GingerAnalyticsConfiguration.IdentityServiceURL));
xClientIdTextBox.Init(mContext, gingerAnalyticsUserConfig, nameof(GingerAnalyticsConfiguration.ClientId));
xClientSecretTextBox.Init(mContext, gingerAnalyticsUserConfig, nameof(GingerAnalyticsConfiguration.ClientSecret));
ApplyValidationRules();

}

private void ApplyValidationRules()
{
// check if fields have been populated (font-end validation)
xGAAURLTextBox.ValueTextBox.AddValidationRule(new ValidateEmptyValue("Report URL cannot be empty"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check if report in error message is correct cause no report url is being used.

xISURLTextBox.ValueTextBox.AddValidationRule(new ValidateEmptyValue("Identitiy Service URL cannot be empty"));
xClientIdTextBox.ValueTextBox.AddValidationRule(new ValidateEmptyValue("ClientID cannot be empty"));
xClientSecretTextBox.ValueTextBox.AddValidationRule(new ValidateEmptyValue("ClientSecret cannot be empty"));
}

private async void xTestConBtn_Click(object sender, RoutedEventArgs e)
{
ShowLoader();
xTestConBtn.IsEnabled = false;
if (AreRequiredFieldsEmpty())
{
Reporter.ToUser(eUserMsgKey.RequiredFieldsEmpty);
HideLoader();
xTestConBtn.IsEnabled = true;
return;
}

GingerCoreNET.GeneralLib.General.CreateGingerAnalyticsConfiguration(gingerAnalyticsUserConfig);

if (GingerAnalyticsAPI.IsTokenValid())
{
Reporter.ToUser(eUserMsgKey.GingerAnalyticsConnectionSuccess);
HideLoader();
xTestConBtn.IsEnabled = true;
return;
}

bool isAuthorized = await HandleTokenAuthorization();
ShowConnectionResult(isAuthorized);
HideLoader();
xTestConBtn.IsEnabled = true;
}

public bool AreRequiredFieldsEmpty()
{
return string.IsNullOrEmpty(gingerAnalyticsUserConfig.AccountUrl)
|| string.IsNullOrEmpty(gingerAnalyticsUserConfig.IdentityServiceURL)
|| string.IsNullOrEmpty(gingerAnalyticsUserConfig.ClientId)
|| string.IsNullOrEmpty(gingerAnalyticsUserConfig.ClientSecret);
}

public async Task<bool> HandleTokenAuthorization()
{
if (string.IsNullOrEmpty(gingerAnalyticsUserConfig.Token))
{
return await GingerAnalyticsAPI.RequestToken(ValueExpression.PasswordCalculation(gingerAnalyticsUserConfig.ClientId),
ValueExpression.PasswordCalculation(gingerAnalyticsUserConfig.ClientSecret),
ValueExpression.PasswordCalculation(gingerAnalyticsUserConfig.IdentityServiceURL));
}

return true;
}

public static void ShowConnectionResult(bool isAuthorized)
{
if (isAuthorized)
{
Reporter.ToUser(eUserMsgKey.GingerAnalyticsConnectionSuccess);
}
else
{
Reporter.ToUser(eUserMsgKey.GingerAnalyticsConnectionFail);
}
}


private void xClientSecretTextBox_LostKeyboardFocus(object sender, System.Windows.Input.KeyboardFocusChangedEventArgs e)
{
if (!EncryptionHandler.IsStringEncrypted(xClientSecretTextBox.ValueTextBox.Text))
{
xClientSecretTextBox.ValueTextBox.Text = ValueExpression.IsThisAValueExpression(xClientSecretTextBox.ValueTextBox.Text) ? xClientSecretTextBox.ValueTextBox.Text : EncryptionHandler.EncryptwithKey(xClientSecretTextBox.ValueTextBox.Text);

}
}

private void xClientIdTextBox_LostKeyboardFocus(object sender, System.Windows.Input.KeyboardFocusChangedEventArgs e)
{
if (!EncryptionHandler.IsStringEncrypted(xClientIdTextBox.ValueTextBox.Text))
{
xClientIdTextBox.ValueTextBox.Text = ValueExpression.IsThisAValueExpression(xClientIdTextBox.ValueTextBox.Text) ? xClientIdTextBox.ValueTextBox.Text : EncryptionHandler.EncryptwithKey(xClientIdTextBox.ValueTextBox.Text);

}
}
Comment on lines +140 to +156
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Encrypt sensitive data appropriately and handle potential security issues.

The encryption logic in xClientSecretTextBox_LostKeyboardFocus and xClientIdTextBox_LostKeyboardFocus methods checks if the string is already encrypted before encrypting it. This is good, but consider the following:

  • Ensure that EncryptionHandler.IsStringEncrypted reliably detects all forms of encrypted strings to prevent double encryption.
  • Consider what happens if encryption fails; currently, there's no error handling for this case.

Add error handling around the encryption logic to ensure the application remains stable and secure if encryption fails.





public void HideLoader()
{
this.Dispatcher.Invoke(() =>
{
xProcessingImage.Visibility = Visibility.Hidden;
});
}

public void ShowLoader()
{
this.Dispatcher.Invoke(() =>
{
xProcessingImage.Visibility = Visibility.Visible;
});
}
}
}
Loading
Loading