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

[AC-1454] Migrate 2fa.directory call #71

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open

Conversation

lizard-boy
Copy link

@lizard-boy lizard-boy commented Oct 19, 2024

Type of change

  • Bug fix
  • New feature development
  • Tech debt (refactoring, code cleanup, dependency upgrades, etc)
  • Build/deploy pipeline (DevOps)
  • Other

Objective

We currently call the 2fa.directory API every time a user generates the Inactive Two Factor report which isn't a very friendly approach. This PR now calls our backend instead. [Current settings] - we will cache the parsed results for 24 hours so we only hit the 2fa.directory API once/day.

Code changes

  • ReportsController.cs: Created Reports API controller with the future in mind of potentially moving around other report functions to the backend.
  • InactiveTwoFactorResponseModel.cs: Created response model containing a read only dictionary so the client doesn't have to do any further logic parsing in regards to the 2fa.directory data
  • GlobalSettings.cs: Added ITwoFactorDirectorySettings for potential customizations to the URL and cache expiration
  • ITwoFactorDirectorySettings.cs: Created settings object for use within GlobalSettings
  • TwoFactorDirectoryTotpResponseModel.cs: Created response model with desired fields for our flattened dictionary
  • GetInactiveTwoFactorQuery.cs: Created query to handle returning either a cached dictionary or a freshly parsed response from the 2fa.directory API. @r-tome created custom parser to handle the response json structure.
  • ToolsServiceCollectionExtensions.cs: Created collection extension to better organize future tools team specific services
  • ServiceCollectionExtensions.cs: Injected new tools services
  • test/*/GetInactiveTwoFactorQueryTests.cs: Created unit tests for the new query.

Shout-outs

Before you submit

  • Please check for formatting errors (dotnet format --verify-no-changes) (required)
  • If making database changes - make sure you also update Entity Framework queries and/or migrations
  • Please add unit tests where it makes sense to do so (encouraged but not required)
  • If this change requires a documentation update - notify the documentation team
  • If this change has particular deployment requirements - notify the DevOps team

Greptile Summary

This pull request implements a caching mechanism for the inactive two-factor authentication report, reducing API calls to the 2fa.directory service.

  • Added ReportsController in /src/Api/Tools/Controllers/ReportsController.cs to handle caching and retrieval of 2FA report data
  • Introduced GetInactiveTwoFactorQuery in /src/Core/Tools/Queries/GetInactiveTwoFactorQuery.cs for fetching and caching 2FA directory data
  • Created TwoFactorDirectorySettings in /src/Core/Settings/GlobalSettings.cs to manage API URL and cache expiration
  • Added unit tests in /test/Core.Test/Tools/Queries/GetInactiveTwoFactorQueryTests.cs to verify caching and API interaction
  • Implemented feature flag MigrateTwoFactorDirectory in /src/Core/Constants.cs for controlled rollout of the new functionality

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

12 file(s) reviewed, 13 comment(s)
Edit PR Review Bot Settings | Greptile

[Authorize("Application")]
public class ReportsController : Controller
{
private readonly ICurrentContext _currentContext;
Copy link

Choose a reason for hiding this comment

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

style: _currentContext is declared but never used

Choose a reason for hiding this comment

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

@greptileai, can I remove this?

var user = await _userService.GetUserByPrincipalAsync(User);
if (!user.Premium)
{
throw new UnauthorizedAccessException("Premium required");
Copy link

Choose a reason for hiding this comment

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

style: Consider using a custom exception for premium requirements

{
public InactiveTwoFactorResponseModel() : base("inactive-two-factor") { }

public IReadOnlyDictionary<string, string> Services { get; set; }
Copy link

Choose a reason for hiding this comment

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

style: Consider using init-only setter for immutability

@@ -0,0 +1,7 @@
namespace Bit.Core.Settings;

public interface ITwoFactorDirectorySettings
Copy link

Choose a reason for hiding this comment

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

style: Consider adding XML documentation comments to describe the interface's purpose

[JsonPropertyName("documentation")]
public string Documentation { get; set; }
[JsonPropertyName("additional-domains")]
public IEnumerable<string> AdditionalDomains { get; set; }
Copy link

Choose a reason for hiding this comment

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

style: Consider using IReadOnlyCollection instead of IEnumerable for AdditionalDomains to prevent accidental modifications

Comment on lines +74 to +88
private static IEnumerable<TwoFactorDirectoryTotpResponseModel> ParseTwoFactorDirectoryTotpResponse(string json)
{
var data = new List<TwoFactorDirectoryTotpResponseModel>();
using var jsonDocument = JsonDocument.Parse(json);
// JSON response object opens with Array notation
if (jsonDocument.RootElement.ValueKind == JsonValueKind.Array)
{
// Each nested array has two values: a floating "name" value [index: 0] and an object with desired data [index: 1]
data.AddRange(from element in jsonDocument.RootElement.EnumerateArray()
where element.ValueKind == JsonValueKind.Array && element.GetArrayLength() == 2
select element[1].Deserialize<TwoFactorDirectoryTotpResponseModel>());
}

return data;
}
Copy link

Choose a reason for hiding this comment

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

style: Consider adding error handling for JSON parsing exceptions


public interface IGetInactiveTwoFactorQuery
{
Task<Dictionary<string, string>> GetInactiveTwoFactorAsync();
Copy link

Choose a reason for hiding this comment

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

style: Consider adding XML documentation comments to describe the method's purpose and return value.

Comment on lines +9 to +12
public static void AddToolsServices(this IServiceCollection services)
{
services.AddReportsQueries();
}
Copy link

Choose a reason for hiding this comment

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

style: Consider adding XML documentation comments to describe the purpose of this public method

sutProvider.GetDependency<IGlobalSettings>().TwoFactorDirectory.Returns(
new GlobalSettings.TwoFactorDirectorySettings()
{
CacheExpirationHours = 1,
Copy link

Choose a reason for hiding this comment

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

logic: The cache expiration is set to 1 hour here, but the PR description mentions 24 hours. Ensure consistency across the implementation.

Uri = new Uri("http://localhost")
});

await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.GetInactiveTwoFactorAsync());
Copy link

Choose a reason for hiding this comment

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

logic: Throwing a BadRequestException for an Unauthorized response might not be the most appropriate exception. Consider using a more specific exception type.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

PR Summary

This pull request implements server-side caching for the 2fa.directory API calls, reducing load on the external service and improving response times for the inactive two-factor authentication report.

  • Added distributed caching in GetInactiveTwoFactorQuery.cs with configurable 24-hour expiration
  • Implemented premium user validation in ReportsController.cs to restrict access to paid accounts
  • Created custom JSON parser in GetInactiveTwoFactorQuery.cs to handle the specific 2fa.directory response format
  • Added comprehensive unit tests covering cache hits, API failures, and successful API responses
  • Protected endpoint with feature flag MigrateTwoFactorDirectory for controlled rollout

12 file(s) reviewed, 10 comment(s)
Edit PR Review Bot Settings | Greptile

Comment on lines +43 to +44

}
Copy link

Choose a reason for hiding this comment

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

style: style: Remove extra blank line before closing brace

{
public InactiveTwoFactorResponseModel() : base("inactive-two-factor") { }

public IReadOnlyDictionary<string, string> Services { get; set; }
Copy link

Choose a reason for hiding this comment

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

logic: Property allows write access despite being IReadOnlyDictionary. Consider making the setter private to ensure immutability.

@@ -115,6 +115,7 @@ public static class FeatureFlagKeys
/// flexible collections
/// </summary>
public const string FlexibleCollectionsMigration = "flexible-collections-migration";
public const string MigrateTwoFactorDirectory = "migrate-two-factor-directory";
Copy link

Choose a reason for hiding this comment

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

style: Consider adding a documentation comment above this constant explaining its purpose, similar to other feature flags in this file

public class TwoFactorDirectorySettings : ITwoFactorDirectorySettings
{
public Uri Uri { get; set; } = new("https://api.2fa.directory/v3/totp.json");
public int CacheExpirationHours { get; set; } = 24;
Copy link

Choose a reason for hiding this comment

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

style: 24 hours may be too long for cache expiration - consider reducing to prevent stale data

Comment on lines +5 to +6
public Uri Uri { get; set; }
public int CacheExpirationHours { get; set; }
Copy link

Choose a reason for hiding this comment

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

style: Consider making properties read-only to prevent unintended modifications after initialization

{
[Required]
[JsonPropertyName("domain")]
public string Domain { get; set; }
Copy link

Choose a reason for hiding this comment

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

style: Domain property should be initialized to avoid null reference exceptions since it's required

Comment on lines +10 to +12
public string Domain { get; set; }
[JsonPropertyName("documentation")]
public string Documentation { get; set; }
Copy link

Choose a reason for hiding this comment

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

style: Consider making properties init-only since they're only populated during deserialization

Comment on lines +41 to +43
using var client = _httpClientFactory.CreateClient();
var response =
await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, _globalSettings.TwoFactorDirectory.Uri));
Copy link

Choose a reason for hiding this comment

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

style: Consider adding a timeout to the HTTP request to prevent hanging on slow responses

.Returns(new HttpResponseMessage()
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("{}", Encoding.UTF8, MediaTypeNames.Application.Json)
Copy link

Choose a reason for hiding this comment

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

style: style: Consider testing with actual 2FA directory response data instead of empty JSON to verify parsing logic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants