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

Cdn improvements #55

Merged
merged 2 commits into from
Nov 19, 2024
Merged

Cdn improvements #55

merged 2 commits into from
Nov 19, 2024

Conversation

0xF6
Copy link
Member

@0xF6 0xF6 commented Nov 19, 2024

Summary by CodeRabbit

  • New Features

    • Enhanced file handling with improved MIME type determination for uploaded files.
    • New method to retrieve user ID from HTTP context claims for better security.
    • Updated Swagger documentation for clearer authentication instructions.
    • Simplified avatar upload methods by removing user ID from route.
    • New service registration for content type provision based on file extensions.
  • Bug Fixes

    • Streamlined signed link generation by removing unnecessary parameters.
  • Refactor

    • Improved clarity and efficiency in various methods across the application.
    • Updated JWT authentication setup for clearer authorization policies.
  • Documentation

    • Enhanced Swagger security scheme for better user guidance.

@0xF6 0xF6 requested a review from urumo November 19, 2024 04:10
@0xF6 0xF6 self-assigned this Nov 19, 2024
Copy link

coderabbitai bot commented Nov 19, 2024

Walkthrough

The changes introduce several updates across multiple files in the Argon.Api project. Key modifications include enhancements to the FilesController for improved file handling and MIME type determination, the addition of user ID retrieval from the HttpContext, and updates to JWT authentication configuration. The CloudflareCaptcha class now uses IHttpContextAccessor for better context management, while the AssetId struct has been updated to handle file extensions. Additionally, the Swagger documentation has been refined, and several service registrations have been modified to improve functionality and clarity.

Changes

File Path Change Summary
src/Argon.Api/Controllers/FilesController.cs Updated constructor to include IContentTypeProvider. Refactored UploadUserAvatar and UploadServerAvatar methods for improved security and clarity.
src/Argon.Api/Extensions/HttpContextExtensions.cs Added GetUserId method to retrieve user ID from claims in HttpContext.
src/Argon.Api/Extensions/SwaggerExtension.cs Modified AddSwaggerWithAuthHeader method to update security definitions for Swagger documentation.
src/Argon.Api/Features/Captcha/CloudflareCaptcha.cs Changed constructor to use IHttpContextAccessor instead of HttpContext.
src/Argon.Api/Features/Jwt/JwtFeature.cs Enhanced JWT authorization configuration by adding a new policy and clarifying method signatures.
src/Argon.Api/Features/MediaStorage/AssetId.cs Updated constructor to include string extensions, removed GetMime method, and modified related methods.
src/Argon.Api/Features/MediaStorage/CdnFeatureExtensions.cs Added service registration for IContentTypeProvider and improved control flow in AddContentDeliveryNetwork.
src/Argon.Api/Features/MediaStorage/YandexContentDeliveryNetwork.cs Simplified GenerateSignedLink method by removing optional ip parameter.
src/Argon.Api/Program.cs Added service registration for IHttpContextAccessor to enable HTTP context injection.

Possibly related PRs

  • Enhance serialization and contracts #12: Changes in the FilesController related to file uploads and MIME types are connected to modifications in the UsersController and IUserManager interface.
  • Feature/fusion auth #13: The introduction of JWT authentication aligns with changes in the JwtExtension and JwtFeature classes, impacting user-related file operations in FilesController.
  • Cut client\server\grains\fusion #39: Changes in user identification and file handling relate to restructuring in the Argon.Entry project, affecting user data management.
  • Feature/fluid and email forms #48: Updates to the FilesController regarding user uploads and MIME types are relevant to changes in the EmailManager and template handling.

Suggested reviewers

  • urumo

Poem

🐇 In the land of code where bunnies hop,
Changes abound, we’ll never stop!
Files now handled with care and grace,
User IDs tucked in a safer place.
With JWTs flying high in the sky,
Let’s celebrate, oh my, oh my! 🎉


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Outside diff range and nitpick comments (13)
src/Argon.Api/Extensions/SwaggerExtension.cs (1)

Line range hint 20-31: Consider documenting the security requirement scope.

While the empty array for scopes is valid, consider adding a comment explaining why no scopes are needed or if they might be added in the future. This will help other developers understand the security configuration.

                        }
                    },
-                    []
+                    Array.Empty<string>() // No scopes needed for JWT Bearer authentication
src/Argon.Api/Features/Captcha/CloudflareCaptcha.cs (2)

7-7: Good improvement using IHttpContextAccessor!

The switch from direct HttpContext to IHttpContextAccessor is a better architectural choice as it:

  • Follows ASP.NET Core best practices for accessing HttpContext
  • Improves testability through proper dependency injection
  • Ensures thread-safety when accessing the HttpContext

Line range hint 10-26: Consider adding rate limiting and additional security measures

While the implementation is solid, consider these security enhancements:

  1. Add rate limiting to prevent brute force attempts
  2. Implement token format validation before making the API call
  3. Consider adding timeout to the HTTP request to prevent hanging connections

Example enhancement:

 public async ValueTask<bool> ValidateAsync(string token)
 {
     if (string.IsNullOrEmpty(token))
         return false;
+    // Basic token format validation (example for Turnstile)
+    if (!token.StartsWith("0x4AAAAAAA") || token.Length < 32)
+        return false;
     var config   = options.Value;
     var remoteIp = accessor.HttpContext!.GetIpAddress();
     try
     {
         var response = await config.ChallengeEndpoint
            .PostMultipartAsync(content => content
                .AddString("secret", config.SiteSecret)
                .AddString("response", token)
                .AddString("remoteip", remoteIp))
+           .WithTimeout(TimeSpan.FromSeconds(5))
            .ReceiveJson<CloudflareTurnstileResponse>();
src/Argon.Api/Extensions/HttpContextExtensions.cs (1)

32-41: Consider aligning error handling with existing patterns

The new GetUserId method handles missing/invalid values differently from other methods in the class. While other methods provide fallback values (e.g., "unknown" for IP), this method throws exceptions. Consider aligning the error handling approach with the existing pattern for consistency.

Options to consider:

  1. Return Guid.Empty for invalid/missing values (following the pattern)
  2. Update other methods to throw exceptions (breaking change)
  3. Add a TryGetUserId method that returns bool (similar to Guid.TryParse pattern)
src/Argon.Api/Features/Jwt/JwtFeature.cs (2)

28-35: Consider revising the authorization policy implementation.

While adding authorization policies is good practice, creating a policy that simply mirrors the authentication scheme doesn't provide additional value. The policy currently only enforces what the authentication middleware already handles.

Consider either:

  1. Removing this policy if no additional requirements are needed, as JWT authentication is already enforced by the authentication middleware, or
  2. Enhancing the policy with specific authorization requirements relevant to your CDN implementation:
 builder.Services.AddAuthorization(options =>
 {
-    options.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy =>
+    options.AddPolicy("CdnAccess", policy =>
     {
         policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
         policy.RequireAuthenticatedUser();
+        policy.RequireClaim("permission", "cdn:access");  // Add specific requirements
     });
 });

Security configuration requires critical updates

Several security concerns need to be addressed:

  • Missing CORS configuration which is required for secure CDN integration
  • No HSTS configuration found, leaving the application vulnerable to protocol downgrade attacks
  • JWT secret key is hardcoded in appsettings files
  • Missing security headers configuration (X-Frame-Options, X-Content-Type-Options, etc.)
  • No rate limiting configuration found to prevent abuse

Required actions:

  1. Add CORS policy configuration with specific allowed origins
  2. Enable HSTS with app.UseHsts()
  3. Move JWT secret to secure configuration management/secrets
  4. Add security headers middleware
  5. Implement rate limiting for authentication endpoints
🔗 Analysis chain

Line range hint 1-44: Verify security configuration completeness.

Let's ensure all security-related configurations are properly set up across the application, particularly for CDN integration.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify security configurations across the application

# Check for CORS configuration which is critical for CDN
echo "Checking CORS configuration..."
rg -l "AddCors|UseCors" --type cs

# Check for security headers middleware
echo "Checking security headers configuration..."
rg -l "UseHsts|UseHttpsRedirection|AddSecurityHeaders" --type cs

# Verify no hardcoded JWT secrets
echo "Checking for potential hardcoded secrets..."
rg -l "SigningKey|Secret|Password" --type cs

# Check for rate limiting configuration
echo "Checking rate limiting configuration..."
rg -l "AddRateLimiting|UseRateLimiting" --type cs

Length of output: 1967


Script:

#!/bin/bash
# Let's examine the security configurations in Program.cs files and check HTTPS/HSTS setup
echo "Checking Program.cs security configurations:"
rg "UseHsts|UseHttpsRedirection" -A 5 -B 5 src/Argon.Entry/Program.cs src/Argon.Api/Program.cs

# Let's check the actual JWT configuration in appsettings
echo "Checking JWT configuration patterns:"
fd appsettings --exec cat {} \; 

# Let's verify CORS setup which is critical for CDN
echo "Checking CORS setup in Program.cs:"
rg "cors|CORS" -i src/Argon.Entry/Program.cs src/Argon.Api/Program.cs

# Let's check if there are any security middleware configurations
echo "Checking security middleware configurations:"
rg "app\.Use|builder\.Services\.Add.*Security" src/Argon.Entry/Program.cs src/Argon.Api/Program.cs

Length of output: 5357

src/Argon.Api/Features/MediaStorage/AssetId.cs (2)

6-6: Document the purpose of the reserved "00" in the file ID format

The comment indicates that the "00" is reserved, but its intended future use isn't clear. This should be documented for maintainability.

-        => $"{assetId:D}-{((byte)scope):X2}-{((byte)kind):X2}-00.{extensions}"; // last two zero reserved
+        => $"{assetId:D}-{((byte)scope):X2}-{((byte)kind):X2}-00.{extensions}"; // Reserved for future versioning or metadata flags

Line range hint 3-52: Improved separation of concerns

The removal of the GetMime method and delegation of MIME type handling to IContentTypeProvider (as mentioned in the summary) is a good architectural decision that better adheres to the Single Responsibility Principle.

src/Argon.Api/Program.cs (1)

32-32: Consider moving HttpContextAccessor registration.

For better code organization, consider moving this registration inside the if (!builder.Environment.IsManaged()) block since it appears to be primarily used by components that are only active in non-managed environments (FilesController, CloudflareCaptcha).

-builder.Services.AddHttpContextAccessor();
 if (!builder.Environment.IsManaged())
 {
+    builder.Services.AddHttpContextAccessor();
     builder.AddJwt();
     builder.Services.AddAuthorization();
src/Argon.Api/Controllers/FilesController.cs (1)

48-60: Address TODO comment and enhance permission check

The permission check could be more comprehensive, and there's an unaddressed TODO comment.

Would you like me to help implement a more detailed permission check system or create an issue to track this TODO?

Additionally, consider enhancing the permission check:

-if (!permissions.CanAccess("server.avatar.upload", PropertyBag.Empty.Set(serverId)))
+if (!permissions.CanAccess("server.avatar.upload", PropertyBag.Empty
+    .Set(serverId)
+    .Set("userId", HttpContext.GetUserId())))
src/Argon.Api/Features/MediaStorage/CdnFeatureExtensions.cs (3)

24-24: Consider configuring custom MIME types

While FileExtensionContentTypeProvider is appropriate for standard MIME types, you might want to configure it for any custom file types specific to your application.

Here's how you could configure custom MIME types:

-builder.Services.TryAddSingleton<IContentTypeProvider, FileExtensionContentTypeProvider>();
+builder.Services.TryAddSingleton<IContentTypeProvider>(serviceProvider => {
+    var provider = new FileExtensionContentTypeProvider();
+    // Add custom mappings if needed
+    // provider.Mappings[".custom"] = "application/x-custom";
+    return provider;
+});

Line range hint 43-48: Refactor to eliminate redundant storage kind checks

The storage kind is already checked in the switch expression above, making these conditions redundant. This could lead to maintenance issues if new StorageKind values are added.

Consider refactoring to this pattern:

-        if (keyName == StorageKind.Disk)
-        {
-            builder.Services.AddKeyedSingleton<IContentStorage, DiskContentStorage>(IContentStorage.DiskContentStorageKey);
-            builder.Services.AddSingleton<IContentDeliveryNetwork, T>();
-        }
-        else if (keyName == StorageKind.GenericS3)
-        {
-            builder.Services.AddKeyedSingleton<IContentStorage, S3ContentStorage>(IContentStorage.GenericS3StorageKey);
-            builder.Services.AddSingleton<IContentDeliveryNetwork, T>();
-            builder.AddS3Storage(keyName, options);
-        }
+        switch (keyName)
+        {
+            case StorageKind.Disk:
+                builder.Services.AddKeyedSingleton<IContentStorage, DiskContentStorage>(IContentStorage.DiskContentStorageKey);
+                builder.Services.AddSingleton<IContentDeliveryNetwork, T>();
+                break;
+            case StorageKind.GenericS3:
+                builder.Services.AddKeyedSingleton<IContentStorage, S3ContentStorage>(IContentStorage.GenericS3StorageKey);
+                builder.Services.AddSingleton<IContentDeliveryNetwork, T>();
+                builder.AddS3Storage(keyName, options);
+                break;
+            default:
+                throw new ArgumentException($"Unsupported storage kind: {keyName}", nameof(keyName));
+        }

This approach:

  1. Eliminates redundant checks
  2. Provides better error messages
  3. Uses pattern matching for cleaner code
  4. Makes it easier to add new storage kinds in the future

Line range hint 54-73: Review lifecycle management of S3 service provider

Creating a separate ServiceProvider for S3 services could lead to lifecycle management issues. The S3 services might not be properly disposed when the application shuts down.

Consider these alternatives:

  1. Register S3 services directly in the main container
  2. If separation is necessary, implement IDisposable on the containing service and properly dispose of the storageServices
  3. Use IServiceScopeFactory for better lifecycle management

Would you like me to provide an example implementation for any of these approaches?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 801cfcc and 11285f8.

📒 Files selected for processing (9)
  • src/Argon.Api/Controllers/FilesController.cs (3 hunks)
  • src/Argon.Api/Extensions/HttpContextExtensions.cs (2 hunks)
  • src/Argon.Api/Extensions/SwaggerExtension.cs (1 hunks)
  • src/Argon.Api/Features/Captcha/CloudflareCaptcha.cs (1 hunks)
  • src/Argon.Api/Features/Jwt/JwtFeature.cs (1 hunks)
  • src/Argon.Api/Features/MediaStorage/AssetId.cs (3 hunks)
  • src/Argon.Api/Features/MediaStorage/CdnFeatureExtensions.cs (3 hunks)
  • src/Argon.Api/Features/MediaStorage/YandexContentDeliveryNetwork.cs (1 hunks)
  • src/Argon.Api/Program.cs (1 hunks)
🔇 Additional comments (10)
src/Argon.Api/Extensions/SwaggerExtension.cs (1)

14-16: LGTM! Standard compliant authentication header configuration.

The changes properly align with standard JWT Bearer authentication practices by using the "Authorization" header.

Let's verify the impact on existing API clients:

✅ Verification successful

The authentication header changes are safe and properly integrated

The search results confirm that:

  1. The JWT Bearer authentication is consistently configured across the codebase:
    • JwtFeature.cs properly sets up JWT Bearer as the default authentication scheme
    • All authenticated endpoints use the standard JwtBearerDefaults.AuthenticationScheme
  2. The "Authorization" header is used correctly in all authentication flows:
    • Swagger configuration aligns with the actual implementation
    • SFU integration also uses standard Bearer token format
  3. No references to the old "x-argon-token" header were found, suggesting this is a new implementation rather than a breaking change
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for any remaining usages of the old header name
# to ensure all clients have been updated

# Search for old header name usage
rg -l "x-argon-token"

# Search for JWT/Bearer token validation logic
ast-grep --pattern 'new JwtBearerOptions { $$$}'

Length of output: 72


Script:

#!/bin/bash
# Let's try a broader search for authentication-related configurations and API clients

# Search for authentication header references
rg -i "authorization.*header" -A 3

# Search for HTTP client configurations
rg -i "httpclient" -A 3

# Search for any Bearer token references
rg -i "bearer" -A 3

# Look for API client classes
fd -e cs --exec grep -l "HttpClient\|IHttpClient"

Length of output: 9961

src/Argon.Api/Features/Captcha/CloudflareCaptcha.cs (1)

14-14: Verify HttpContext null handling and IP address extraction

The current implementation has potential null reference risks:

  1. Using the null-forgiving operator (!) on HttpContext without a null check could lead to runtime exceptions
  2. The GetIpAddress() extension method's implementation needs verification for proper IP extraction, especially with reverse proxies or CDN scenarios

Let's verify the implementation:

✅ Verification successful

The null-forgiving operator usage is acceptable in this context

The implementation is safe because:

  1. HttpContextAccessor is properly registered via AddHttpContextAccessor() in Program.cs
  2. The GetIpAddress() extension method handles null cases gracefully by:
    • Checking Cloudflare headers first
    • Falling back to Connection.RemoteIpAddress with null coalescing
    • Providing "unknown" as a final fallback
  3. The pattern is consistently used across the codebase (AuthorizationController examples)
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check the GetIpAddress extension method implementation
echo "Searching for GetIpAddress implementation..."
rg -l "GetIpAddress"

# Check for other usages of IHttpContextAccessor to compare null handling patterns
echo "Checking other IHttpContextAccessor usage patterns..."
ast-grep --pattern 'accessor.HttpContext'

Length of output: 4772


Script:

#!/bin/bash
# Let's check the implementation of GetIpAddress extension method
echo "GetIpAddress implementation:"
rg -A 10 "GetIpAddress" src/Argon.Api/Extensions/HttpContextExtensions.cs

# Check for null checks in AuthorizationController
echo -e "\nUsage in AuthorizationController:"
rg -A 3 "GetIpAddress" src/Argon.Api/Controllers/AuthorizationController.cs

# Check if there's any middleware or service setup handling null HttpContext
echo -e "\nChecking for HttpContext setup in Program.cs or Startup:"
rg "AddHttpContextAccessor|UseMiddleware" 

Length of output: 1400

src/Argon.Api/Extensions/HttpContextExtensions.cs (2)

3-3: LGTM!

The System.Security.Claims import is appropriate for the new functionality.


32-41: ⚠️ Potential issue

Add authentication check and improve error handling

The current implementation has several potential issues:

  1. No authentication check before accessing claims
  2. Incomplete error message when "id" claim fails
  3. Possible NullReferenceException if ctx.User is null

Consider this improved implementation:

     public static Guid GetUserId(this HttpContext ctx)
     {
+        if (ctx?.User?.Identity?.IsAuthenticated != true)
+            throw new UnauthorizedAccessException("User is not authenticated");
+
         var userId = ctx.User.FindFirstValue(ClaimTypes.NameIdentifier);
-
         userId ??= ctx.User.FindFirstValue("id");
 
         if (Guid.TryParse(userId, out var result))
             return result;
-        throw new FormatException($"UserId by '{ClaimTypes.NameIdentifier} claim has value: '{userId}' - incorrect guid");
+        throw new FormatException($"Invalid user ID format. Value '{userId}' could not be parsed as GUID");
     }

Let's verify how this method is used in the codebase:

src/Argon.Api/Features/MediaStorage/YandexContentDeliveryNetwork.cs (1)

38-38: Verify security implications of removing IP-based restrictions.

The removal of IP-based restrictions from signed URLs simplifies the implementation but might affect security. While the URLs are still protected by expiration time and signatures, consider documenting this architectural decision and its implications.

Let's verify if this change is consistent across the codebase and if there are any security compensating controls:

✅ Verification successful

Removal of IP-based restrictions is an acceptable security trade-off

The codebase analysis shows that:

  1. The interface IContentDeliveryNetwork doesn't mandate IP-based restrictions
  2. The signed URL generation is consistently implemented using expiration time and secret signing
  3. The system uses SimpleS3 authentication mechanisms for the underlying storage

The removal of IP restrictions simplifies the API while maintaining core security through time-based expiration and cryptographic signing, which is a common and secure approach for CDN implementations.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for IP-based security controls and documentation

# Check for any IP-based security controls in the codebase
rg -l "(?i)(ip.*restrict|restrict.*ip|whitelist.*ip|ip.*whitelist)"

# Look for security documentation
fd -g "*security*.md" -g "*readme*.md" -x cat {}

Length of output: 221


Script:

#!/bin/bash
# Let's try a more comprehensive analysis

# 1. Check for any IP-based security controls with broader pattern
rg -i "ip.{0,20}(restrict|allow|block|check)"

# 2. Look for documentation files correctly
fd "README" --type f
fd "SECURITY" --type f

# 3. Check the interface and its implementations
ast-grep --pattern 'interface IContentDeliveryNetwork {
  $$$
}'

# 4. Look for other CDN implementations
fd -e cs -x rg -l "class.*implements.*IContentDeliveryNetwork"

# 5. Check for any security-related configurations
fd -e json -e xml -e config -x rg -l "security|authentication"

Length of output: 288


Script:

#!/bin/bash
# Let's focus on the specific CDN implementation and interface

# 1. Find and show the interface definition
rg -A 10 "interface IContentDeliveryNetwork"

# 2. Look for all implementations of GenerateSignedLink
rg -A 5 "GenerateSignedLink"

# 3. Check for any security-related comments or documentation in the CDN files
rg -i "security|authentication|authorization" src/Argon.Api/Features/MediaStorage/

Length of output: 2204

src/Argon.Api/Features/MediaStorage/AssetId.cs (1)

23-34: LGTM! Improved readability of dictionary initialization

The new formatting makes the dictionary initialization more readable and maintainable.

src/Argon.Api/Program.cs (1)

32-32: Verify CDN integration with HttpContext.

The HttpContextAccessor registration looks good and aligns with the changes in FilesController and CloudflareCaptcha. However, since this PR focuses on CDN improvements, let's verify the CDN integration.

src/Argon.Api/Controllers/FilesController.cs (2)

4-14: LGTM! Constructor changes follow DI best practices

The addition of IContentTypeProvider and related using statements properly supports the new MIME type detection functionality.

Also applies to: 20-21


63-64: Verify JWT authentication configuration

The endpoint uses JWT authentication. Let's verify the authentication setup and user ID claim configuration.

✅ Verification successful

JWT authentication and user ID claim configuration are properly set up

The verification shows that:

  • JWT authentication is correctly configured in JwtFeature.cs with proper default schemes and token validation
  • User ID retrieval is implemented in HttpContextExtensions.GetUserId() with fallback support:
    • Primary: ClaimTypes.NameIdentifier
    • Fallback: id claim
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check JWT authentication configuration and user ID claim setup
rg -A 10 "AddJwtBearer|JwtBearerDefaults" 
rg -A 5 "GetUserId"

Length of output: 4426

src/Argon.Api/Features/MediaStorage/CdnFeatureExtensions.cs (1)

10-11: LGTM: Using directives are properly organized

The new using directives are necessary for the added functionality and follow proper organization (framework imports after domain imports).

builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer((options) => options.TokenValidationParameters = tokenValidator);
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, (options) => options.TokenValidationParameters = tokenValidator);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance JWT configuration with CDN-relevant security headers.

Given this PR's focus on CDN improvements, consider adding security headers to protect against token theft and ensure secure content delivery.

Add these security-focused options to the JWT bearer configuration:

 .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, (options) => 
 {
     options.TokenValidationParameters = tokenValidator;
+    options.IncludeErrorDetails = false; // Security: Don't expose unnecessary error details
+    options.RequireHttpsMetadata = true; // Ensure HTTPS in production
+    options.SaveToken = true; // Required for CDN token forwarding
+    options.Events = new JwtBearerEvents
+    {
+        OnMessageReceived = context =>
+        {
+            // Allow token from CDN-specific custom header
+            var token = context.Request.Headers["X-CDN-Authorization"].FirstOrDefault();
+            if (!string.IsNullOrEmpty(token))
+            {
+                context.Token = token;
+            }
+            return Task.CompletedTask;
+        }
+    };
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, (options) => options.TokenValidationParameters = tokenValidator);
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, (options) =>
{
options.TokenValidationParameters = tokenValidator;
options.IncludeErrorDetails = false; // Security: Don't expose unnecessary error details
options.RequireHttpsMetadata = true; // Ensure HTTPS in production
options.SaveToken = true; // Required for CDN token forwarding
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
// Allow token from CDN-specific custom header
var token = context.Request.Headers["X-CDN-Authorization"].FirstOrDefault();
if (!string.IsNullOrEmpty(token))
{
context.Token = token;
}
return Task.CompletedTask;
}
};
});

{
var expires = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + expiryInSeconds;
var tokenData = expires + path + (ip ?? "") + secret;
var tokenData = $"{expires}{path} {secret}";
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Security: Strengthen the token generation implementation.

The current implementation has several security considerations:

  1. MD5 is cryptographically weak and should be replaced with a stronger algorithm.
  2. Manual Base64 URL-safe encoding could be replaced with built-in methods.
  3. String concatenation of sensitive data might be vulnerable to timing attacks.

Consider applying these security improvements:

-        var tokenData = $"{expires}{path} {secret}";
-
-        using var md5       = MD5.Create();
-        var       hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(tokenData));
-        var token = Convert.ToBase64String(hashBytes)
-           .Replace("\n", "")
-           .Replace("+", "-")
-           .Replace("/", "_")
-           .Replace("=", "");
+        byte[] pathBytes = Encoding.UTF8.GetBytes(path);
+        byte[] expiresBytes = BitConverter.GetBytes(expires);
+        
+        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
+        hmac.TransformBlock(expiresBytes, 0, expiresBytes.Length, null, 0);
+        hmac.TransformFinalBlock(pathBytes, 0, pathBytes.Length);
+        
+        var token = WebEncoders.Base64UrlEncode(hmac.Hash!);

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +51 to +52
public static AssetId Avatar() => new(Guid.NewGuid(), AssetScope.ProfileAsset, AssetKind.Image, "png");
public static AssetId VideoAvatar() => new(Guid.NewGuid(), AssetScope.ProfileAsset, AssetKind.VideoNoSound, "mp4");
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

⚠️ Potential issue

Inconsistency found between VideoAvatar implementation and AssetKind enum documentation

The verification confirms there's a clear discrepancy:

  • AssetKind.VideoNoSound enum is documented to use "gif" format in the comment
  • VideoAvatar() method implements it with "mp4" format

This inconsistency needs to be resolved by either:

  • Updating the enum comment to reflect the actual mp4 implementation, or
  • Changing the VideoAvatar implementation to use gif format as per the enum documentation

The suggestion to make extensions configurable remains valid as it would provide more flexibility.

🔗 Analysis chain

Resolve discrepancy between enum comment and implementation

There's a mismatch between the AssetKind.VideoNoSound enum comment (which suggests "gif") and the VideoAvatar() implementation (which uses "mp4").

Additionally, consider making the extensions configurable:

+    private const string DEFAULT_IMAGE_EXT = "png";
+    private const string DEFAULT_VIDEO_EXT = "mp4";
+
-    public static AssetId Avatar()      => new(Guid.NewGuid(), AssetScope.ProfileAsset, AssetKind.Image, "png");
-    public static AssetId VideoAvatar() => new(Guid.NewGuid(), AssetScope.ProfileAsset, AssetKind.VideoNoSound, "mp4");
+    public static AssetId Avatar(string extension = DEFAULT_IMAGE_EXT)      
+        => new(Guid.NewGuid(), AssetScope.ProfileAsset, AssetKind.Image, extension);
+    public static AssetId VideoAvatar(string extension = DEFAULT_VIDEO_EXT) 
+        => new(Guid.NewGuid(), AssetScope.ProfileAsset, AssetKind.VideoNoSound, extension);
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any other usages of VideoNoSound to understand the correct format
rg -l "VideoNoSound" | xargs rg -A 5 "VideoNoSound"

Length of output: 311

Comment on lines +3 to +6
public readonly struct AssetId(Guid assetId, AssetScope scope, AssetKind kind, string extensions)
{
public string ToFileId()
=> $"{assetId:D}-{((byte)scope):X2}-{((byte)kind):X2}-00"; // last two zero reserved
=> $"{assetId:D}-{((byte)scope):X2}-{((byte)kind):X2}-00.{extensions}"; // last two zero reserved
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add input validation for the extensions parameter

The extensions parameter should be validated to prevent potential issues with empty or invalid file extensions.

 public readonly struct AssetId(Guid assetId, AssetScope scope, AssetKind kind, string extensions)
 {
+    private readonly string _extensions = string.IsNullOrWhiteSpace(extensions)
+        ? throw new ArgumentException("Extension cannot be empty", nameof(extensions))
+        : extensions.TrimStart('.');
+
     public string ToFileId()
-        => $"{assetId:D}-{((byte)scope):X2}-{((byte)kind):X2}-00.{extensions}";
+        => $"{assetId:D}-{((byte)scope):X2}-{((byte)kind):X2}-00.{_extensions}";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public readonly struct AssetId(Guid assetId, AssetScope scope, AssetKind kind, string extensions)
{
public string ToFileId()
=> $"{assetId:D}-{((byte)scope):X2}-{((byte)kind):X2}-00"; // last two zero reserved
=> $"{assetId:D}-{((byte)scope):X2}-{((byte)kind):X2}-00.{extensions}"; // last two zero reserved
public readonly struct AssetId(Guid assetId, AssetScope scope, AssetKind kind, string extensions)
{
private readonly string _extensions = string.IsNullOrWhiteSpace(extensions)
? throw new ArgumentException("Extension cannot be empty", nameof(extensions))
: extensions.TrimStart('.');
public string ToFileId()
=> $"{assetId:D}-{((byte)scope):X2}-{((byte)kind):X2}-00.{_extensions}"; // last two zero reserved

Comment on lines +47 to +48
var ext = fileId.Split('.').Last();
return new AssetId(guid, (AssetScope)scope, (AssetKind)kind, ext);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Optimize extension extraction and add validation

The current implementation has potential performance and reliability issues:

  1. Using Split() creates unnecessary array allocations
  2. No validation for the presence of an extension
  3. Could break with multiple dots in filename
-        var ext   = fileId.Split('.').Last();
-        return new AssetId(guid, (AssetScope)scope, (AssetKind)kind, ext);
+        var lastDotIndex = fileId.LastIndexOf('.');
+        if (lastDotIndex == -1 || lastDotIndex == fileId.Length - 1)
+            throw new InvalidOperationException("File ID must contain a valid extension");
+        var ext = fileId[(lastDotIndex + 1)..];
+        return new AssetId(guid, (AssetScope)scope, (AssetKind)kind, ext);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var ext = fileId.Split('.').Last();
return new AssetId(guid, (AssetScope)scope, (AssetKind)kind, ext);
var lastDotIndex = fileId.LastIndexOf('.');
if (lastDotIndex == -1 || lastDotIndex == fileId.Length - 1)
throw new InvalidOperationException("File ID must contain a valid extension");
var ext = fileId[(lastDotIndex + 1)..];
return new AssetId(guid, (AssetScope)scope, (AssetKind)kind, ext);

Comment on lines +35 to +41
var ns = new StorageNameSpace(nsPath, nsId);
var assetId = AssetId.FromFileId(fileId);
var mem = DiskContentStorage.OpenFileRead(ns, assetId);

return File(mem, assetId.GetMime());
if (contentType.TryGetContentType(fileId, out var mime))
return File(mem, mime);
return File(mem, "application/octet-stream");
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add null check and error handling for file operations

While the MIME type detection is well implemented, consider adding proper error handling for cases where the file doesn't exist or cannot be read.

 var ns      = new StorageNameSpace(nsPath, nsId);
 var assetId = AssetId.FromFileId(fileId);
+try {
     var mem     = DiskContentStorage.OpenFileRead(ns, assetId);
+    if (mem == null)
+        return NotFound();
     
     if (contentType.TryGetContentType(fileId, out var mime))
         return File(mem, mime);
     return File(mem, "application/octet-stream");
+} catch (IOException ex) {
+    return StatusCode(500, "Unable to read file");
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var ns = new StorageNameSpace(nsPath, nsId);
var assetId = AssetId.FromFileId(fileId);
var mem = DiskContentStorage.OpenFileRead(ns, assetId);
return File(mem, assetId.GetMime());
if (contentType.TryGetContentType(fileId, out var mime))
return File(mem, mime);
return File(mem, "application/octet-stream");
var ns = new StorageNameSpace(nsPath, nsId);
var assetId = AssetId.FromFileId(fileId);
try {
var mem = DiskContentStorage.OpenFileRead(ns, assetId);
if (mem == null)
return NotFound();
if (contentType.TryGetContentType(fileId, out var mime))
return File(mem, mime);
return File(mem, "application/octet-stream");
} catch (IOException ex) {
return StatusCode(500, "Unable to read file");
}

Comment on lines +63 to +77
[HttpPost("/files/user/@me/avatar"), Authorize(JwtBearerDefaults.AuthenticationScheme)]
public async ValueTask<IActionResult> UploadUserAvatar(IFormFile file)
{
// TODO
if (!permissions.CanAccess("user.avatar.upload", PropertyBag.Empty.Set(userId)))
return StatusCode(401);
var userId = HttpContext.GetUserId();
var assetId = AssetId.Avatar();
var result = await cdn.CreateAssetAsync(StorageNameSpace.ForUser(userId), assetId, file);
var ns = StorageNameSpace.ForUser(userId);
var result = await cdn.CreateAssetAsync(ns, assetId, file);

if (result.HasValue)
return Ok(result);

await grainFactory.GetGrain<IUserGrain>(userId)
.UpdateUser(new UserEditInput(null, null, assetId.ToFileId()));

return Ok();
return Ok(await cdn.GenerateAssetUrl(ns, assetId));
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add file validation for avatar uploads

While the user ID handling has been improved, consider adding file validation to ensure security and consistency.

 public async ValueTask<IActionResult> UploadUserAvatar(IFormFile file)
 {
+    if (file == null || file.Length == 0)
+        return BadRequest("No file uploaded");
+        
+    if (file.Length > 5 * 1024 * 1024) // 5MB limit
+        return BadRequest("File size exceeds limit");
+        
+    if (!file.ContentType.StartsWith("image/"))
+        return BadRequest("Only image files are allowed");
+        
     var userId  = HttpContext.GetUserId();
     // ... rest of the method
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[HttpPost("/files/user/@me/avatar"), Authorize(JwtBearerDefaults.AuthenticationScheme)]
public async ValueTask<IActionResult> UploadUserAvatar(IFormFile file)
{
// TODO
if (!permissions.CanAccess("user.avatar.upload", PropertyBag.Empty.Set(userId)))
return StatusCode(401);
var userId = HttpContext.GetUserId();
var assetId = AssetId.Avatar();
var result = await cdn.CreateAssetAsync(StorageNameSpace.ForUser(userId), assetId, file);
var ns = StorageNameSpace.ForUser(userId);
var result = await cdn.CreateAssetAsync(ns, assetId, file);
if (result.HasValue)
return Ok(result);
await grainFactory.GetGrain<IUserGrain>(userId)
.UpdateUser(new UserEditInput(null, null, assetId.ToFileId()));
return Ok();
return Ok(await cdn.GenerateAssetUrl(ns, assetId));
[HttpPost("/files/user/@me/avatar"), Authorize(JwtBearerDefaults.AuthenticationScheme)]
public async ValueTask<IActionResult> UploadUserAvatar(IFormFile file)
{
if (file == null || file.Length == 0)
return BadRequest("No file uploaded");
if (file.Length > 5 * 1024 * 1024) // 5MB limit
return BadRequest("File size exceeds limit");
if (!file.ContentType.StartsWith("image/"))
return BadRequest("Only image files are allowed");
var userId = HttpContext.GetUserId();
var assetId = AssetId.Avatar();
var ns = StorageNameSpace.ForUser(userId);
var result = await cdn.CreateAssetAsync(ns, assetId, file);
if (result.HasValue)
return Ok(result);
await grainFactory.GetGrain<IUserGrain>(userId)
.UpdateUser(new UserEditInput(null, null, assetId.ToFileId()));
return Ok(await cdn.GenerateAssetUrl(ns, assetId));

@urumo urumo merged commit 610b186 into master Nov 19, 2024
4 checks passed
@urumo urumo deleted the cdn-improvements branch November 19, 2024 14:25
@argon-chat argon-chat locked and limited conversation to collaborators Nov 19, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants