Add authorization to views#3284
Conversation
…each action/routes
| <MudCardHeader Class="DeviceCardHeader"> | ||
| <CardHeaderContent> | ||
| <MudText Typo="Typo.body2" Class="mb-6" id=@nameof(IoTEdgeModel.Name)>Model: <b>@edgeModel?.Name</b></MudText> | ||
| <MudText Typo="Typo.h5" Class="overflow-ellipsis" Align="Align.Center">@(string.IsNullOrEmpty(EdgeDevice?.DeviceName) ? EdgeDevice?.DeviceId : EdgeDevice?.DeviceName)</MudText> |
Check warning
Code scanning / CodeQL
Constant condition Warning
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 months ago
In general, to fix a “constant condition” that arises from a null-check being effectively redundant, you should remove the unnecessary null-conditional usage and rely on a single, clear condition (or on established invariants) instead of sprinkling ?. everywhere. The goal is to preserve behavior while making the analyzer understand that the condition is meaningful, not constant.
In this specific case, the problematic line is:
<MudText Typo="Typo.h5" Class="overflow-ellipsis" Align="Align.Center">@(string.IsNullOrEmpty(EdgeDevice?.DeviceName) ? EdgeDevice?.DeviceId : EdgeDevice?.DeviceName)</MudText>We can keep the intended logic—“show the device name if not empty; otherwise show the device ID”—without repeatedly using the null-conditional on EdgeDevice. Assuming EdgeDevice is always initialized for this form (which is consistent with typical Razor page models), we can drop the ?. on EdgeDevice itself while still being safe against DeviceName being null or empty, and against DeviceId potentially being null. That gives:
@(string.IsNullOrEmpty(EdgeDevice.DeviceName) ? EdgeDevice.DeviceId : EdgeDevice.DeviceName)This preserves the behavior: if DeviceName is null or empty, show DeviceId; otherwise show DeviceName. The only change is removing the unnecessary null-conditionals that lead the analyzer to infer a constant nullness state. No new methods, imports, or other definitions are required; it’s a single-line edit within CreateEdgeDevicePage.razor.
| @@ -23,7 +23,7 @@ | ||
| <MudCardHeader Class="DeviceCardHeader"> | ||
| <CardHeaderContent> | ||
| <MudText Typo="Typo.body2" Class="mb-6" id=@nameof(IoTEdgeModel.Name)>Model: <b>@edgeModel?.Name</b></MudText> | ||
| <MudText Typo="Typo.h5" Class="overflow-ellipsis" Align="Align.Center">@(string.IsNullOrEmpty(EdgeDevice?.DeviceName) ? EdgeDevice?.DeviceId : EdgeDevice?.DeviceName)</MudText> | ||
| <MudText Typo="Typo.h5" Class="overflow-ellipsis" Align="Align.Center">@(string.IsNullOrEmpty(EdgeDevice.DeviceName) ? EdgeDevice.DeviceId : EdgeDevice.DeviceName)</MudText> | ||
| </CardHeaderContent> | ||
| </MudCardHeader> | ||
| <MudCardContent> |
| foreach (var permission in PortalPermissionsHelper.GetAllPermissionStrings()) | ||
| { | ||
| // Avoid duplicate registration (e.g. if called twice in tests) | ||
| if (options.GetPolicy(permission) is null) | ||
| { | ||
| options.AddPolicy(permission, policy => | ||
| policy.Requirements.Add(new PermissionRequirement(permission))); | ||
| } | ||
| } |
Check notice
Code scanning / CodeQL
Missed opportunity to use Where Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 months ago
In general, to fix this pattern you apply a .Where(...) filter to the sequence you’re iterating, moving the condition from inside the loop into the LINQ predicate. Then you remove the now-redundant if and its braces, keeping the loop body unchanged otherwise.
Here, we should filter PortalPermissionsHelper.GetAllPermissionStrings() so that the loop only runs for permissions where options.GetPolicy(permission) is null. The best non-invasive fix is to change line 20 to iterate over PortalPermissionsHelper.GetAllPermissionStrings().Where(permission => options.GetPolicy(permission) is null) and then remove the if/block on lines 23–27, leaving just the AddPolicy call directly in the loop body. We must add using System.Linq; at the top of the file (within the shown code) so that .Where(...) is available. No other behavior changes are needed.
Specifically:
- In
src/IoTHub.Portal.Server/Security/PortalPermissions.cs, addusing System.Linq;alongside existingusingdirectives. - Replace the
foreachheader and guarded body with aforeachover aWhere-filtered sequence and an unguardedoptions.AddPolicy(...)call.
| @@ -5,6 +5,7 @@ | ||
| { | ||
| using Microsoft.AspNetCore.Authorization; | ||
| using Shared.Helpers; | ||
| using System.Linq; | ||
|
|
||
| /// <summary> | ||
| /// Central place where all portal permissions (policies) are declared. | ||
| @@ -17,14 +18,12 @@ | ||
| /// </summary> | ||
| public static void AddPolicies(AuthorizationOptions options) | ||
| { | ||
| foreach (var permission in PortalPermissionsHelper.GetAllPermissionStrings()) | ||
| foreach (var permission in PortalPermissionsHelper.GetAllPermissionStrings() | ||
| .Where(permission => options.GetPolicy(permission) is null)) | ||
| { | ||
| // Avoid duplicate registration (e.g. if called twice in tests) | ||
| if (options.GetPolicy(permission) is null) | ||
| { | ||
| options.AddPolicy(permission, policy => | ||
| policy.Requirements.Add(new PermissionRequirement(permission))); | ||
| } | ||
| options.AddPolicy(permission, policy => | ||
| policy.Requirements.Add(new PermissionRequirement(permission))); | ||
| } | ||
| } | ||
| } |
| catch (Exception ex) | ||
| { | ||
| Snackbar.Add($"Failed to load users: {ex.Message}", MudBlazor.Severity.Error); | ||
| return new TableData<UserModel> { Items = Array.Empty<UserModel>(), TotalItems = 0 }; | ||
| } |
Check notice
Code scanning / CodeQL
Generic catch clause Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 months ago
In general, to fix a “generic catch clause” issue, you should catch only the specific exception types you expect and know how to handle. Reserve truly generic catch blocks for very top-level boundaries (e.g., global exception handlers), and even then consider logging and rethrow/termination as appropriate.
For this file, the best targeted fix that preserves existing behavior is:
- Replace
catch (Exception ex)with more specific catches. Given the context (an HTTP-like client service call from UI), the most likely expected exception isHttpRequestException(or potentially a custom service exception). We can:- First catch
HttpRequestExceptionand show the same snackbar + empty result. - Optionally add a second broader
catchthat still logs/shows the message but is clearly marked as last-resort. However, to satisfy the rule and avoid swallowing all bugs, we should avoid catching plainExceptionwhere possible. With the information provided, we’ll narrow toHttpRequestExceptiononly.
- First catch
- Add any required
using(here,System.Net.Http) at the top of the Razor file if it’s not already present. Since we’re allowed to introduce imports of well-known libraries, this is safe.
Concretely:
- In
src/IoTHub.Portal.Client/Pages/RBAC/UsersListPage.razor, replace lines 69–73:- Change
catch (Exception ex)tocatch (HttpRequestException ex).
- Change
- Add a
@using System.Net.Httpdirective near the top of the Razor file soHttpRequestExceptionis in scope.
This keeps functionality the same for typical network/service failures while no longer generically catching every possible exception.
| @@ -1,6 +1,7 @@ | ||
| @page "/users" | ||
| @inherits AuthorizedComponentBase | ||
| @using IoTHub.Portal.Shared.Security | ||
| @using System.Net.Http | ||
|
|
||
| @attribute [Authorize] | ||
|
|
||
| @@ -66,7 +67,7 @@ | ||
| var result = await UserClientService.GetUsers(uri); | ||
| return new TableData<UserModel> { Items = result.Items, TotalItems = result.TotalItems }; | ||
| } | ||
| catch (Exception ex) | ||
| catch (HttpRequestException ex) | ||
| { | ||
| Snackbar.Add($"Failed to load users: {ex.Message}", MudBlazor.Severity.Error); | ||
| return new TableData<UserModel> { Items = Array.Empty<UserModel>(), TotalItems = 0 }; |
| if (val) | ||
| _ = SelectedPermissions.Add(permission); | ||
| else | ||
| _ = SelectedPermissions.Remove(permission); |
Check notice
Code scanning / CodeQL
Missed ternary opportunity Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 months ago
Generally, to fix this pattern you replace an if/else that solely assigns or returns values with a single expression using the conditional (? :) operator, preserving behavior while shortening and clarifying the intent. In this specific case, we can select between SelectedPermissions.Add(permission) and SelectedPermissions.Remove(permission) via a conditional expression rather than an if/else.
Within src/IoTHub.Portal.Client/Components/Roles/EditRole.razor, in the TogglePermission method (around lines 170–176), we replace:
if (val)
_ = SelectedPermissions.Add(permission);
else
_ = SelectedPermissions.Remove(permission);with a single statement that uses the ternary operator. Because both Add and Remove return a bool that is discarded, we can write:
_ = val ? SelectedPermissions.Add(permission) : SelectedPermissions.Remove(permission);This keeps the same semantics (calling exactly one of the two methods based on val), keeps types consistent (bool on both branches), and removes the explicit if/else. No additional imports, helper methods, or definitions are needed; we only change that short region.
| @@ -169,10 +169,7 @@ | ||
|
|
||
| private void TogglePermission(string permission, bool val) | ||
| { | ||
| if (val) | ||
| _ = SelectedPermissions.Add(permission); | ||
| else | ||
| _ = SelectedPermissions.Remove(permission); | ||
| _ = val ? SelectedPermissions.Add(permission) : SelectedPermissions.Remove(permission); | ||
| } | ||
|
|
||
| private string GetMultiSelectionText(IEnumerable<string> selectedValues) |
| catch (Exception ex) | ||
| { | ||
| Snackbar.Add($"Erreur chargement permissions : {ex.Message}", Severity.Error); | ||
| AllPermissions = Array.Empty<string>(); | ||
| } |
Check notice
Code scanning / CodeQL
Generic catch clause Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 months ago
In general, the fix is to replace the generic catch (Exception ex) with more specific exception types that represent expected, recoverable failures (e.g., communication or domain exceptions thrown by RoleClientService), and to avoid treating things like task cancellation as an error. For unexpected exceptions, either let them bubble up or at least separate their handling so they’re not silently normalized into the same user message as ordinary API errors.
In this file, the main problematic clause is in LoadPermissions. The smallest change that improves correctness without altering user-visible behavior too much is:
- Add a dedicated
catch (OperationCanceledException)(orTaskCanceledException) that simply returns, so that cancellation doesn’t trigger an error snackbar or overwriteAllPermissions. - Keep a narrower
catchfor expected failures. Because we don’t see the definition ofRoleClientService, we shouldn’t invent custom exception types; instead, we can:- Catch
HttpRequestException(common for HTTP-based client services) for user-facing “load failed” messages. - Let other exceptions propagate, which is safer than silently converting them into “Erreur chargement permissions…”.
- Catch
However, the instructions say we should not assume too much about the rest of the code and should prefer well-known libraries. HttpRequestException is part of System.Net.Http (well-known), but we also must not change imports we haven’t seen. Since this is a Razor file, using HttpRequestException without an explicit using is legal if fully qualified, but that’s noisy. To stay minimal and safe, we can at least:
- Separate
OperationCanceledException(do nothing special). - Keep a generic
catch (Exception ex)but narrow the scope by:- Moving only
RoleClientService.GetPermissions()into the try-block and doing the LINQ transform outside, or - Filtering out
OperationCanceledExceptionvia a when-clause.
- Moving only
Filtering via when is the least invasive functional change and keeps the same overall behavior for all non-cancellation failures. We can write:
catch (OperationCanceledException)
{
// Allow cancellation to propagate without showing an error or modifying permissions
throw;
}
catch (Exception ex)
{
Snackbar.Add(...);
AllPermissions = Array.Empty<string>();
}This technically still uses a generic catch, so CodeQL may still flag it. A better compromise (and still quite safe) is:
catch (OperationCanceledException)— rethrow or just return.catchonly domain-appropriate, well-known exceptionHttpRequestExceptionfor API failures.- Let anything else bubble up.
That removes the generic catch entirely and is the clearest fix in line with the recommendation.
Concretely, in LoadPermissions (lines 151–168), we will:
- Replace the single
catch (Exception ex)with:catch (OperationCanceledException)that simply returns so cancellation is not treated as an error.catch (HttpRequestException ex)that shows the snackbar and setsAllPermissionsto empty.
- Leave the
finallyblock unchanged.
No other methods in the provided snippet have a generic catch; the Save method has one too (catch (Exception ex) at line 201). For consistency and to satisfy the analyzer, we should also narrow that one similarly (e.g., HttpRequestException plus maybe OperationCanceledException), but we must be careful not to change semantics much: cancellation shouldn’t show an error; HTTP failures should.
| @@ -155,8 +155,13 @@ | ||
| isLoadingPermissions = true; | ||
| AllPermissions = (await RoleClientService.GetPermissions()).Select(x => x.AsString()).ToArray(); | ||
| } | ||
| catch (Exception ex) | ||
| catch (OperationCanceledException) | ||
| { | ||
| // Do not treat cancellations as errors; simply stop loading. | ||
| return; | ||
| } | ||
| catch (HttpRequestException ex) | ||
| { | ||
| Snackbar.Add($"Erreur chargement permissions : {ex.Message}", Severity.Error); | ||
| AllPermissions = Array.Empty<string>(); | ||
| } | ||
| @@ -198,8 +202,13 @@ | ||
|
|
||
| await OnSaved.InvokeAsync(); | ||
| } | ||
| catch (Exception ex) | ||
| catch (OperationCanceledException) | ||
| { | ||
| // Operation was cancelled (e.g., navigation away); do not show an error. | ||
| return; | ||
| } | ||
| catch (HttpRequestException ex) | ||
| { | ||
| Snackbar.Add($"Erreur : {ex.Message}", Severity.Error); | ||
| } | ||
| finally |
| catch (Exception ex) | ||
| { | ||
| Logger.LogError(ex, "Error checking permission {Permission}", permission); | ||
| return false; | ||
| } |
Check notice
Code scanning / CodeQL
Generic catch clause Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 months ago
In general, to fix a generic catch clause you identify the specific exception types that are expected from the operations in the try block and catch only those, optionally letting other, unexpected exceptions bubble up. This avoids unintentionally masking programming errors or unrelated failures.
Here, HasPermissionAsync is calling PermissionsService.GetUserPermissions(), which is likely to throw HttpRequestException for network / HTTP problems and possibly specific status codes like 401/403. The rest of the code in this class already singles out HttpRequestException with StatusCode filters in CheckAuthorizationAsync. The safest minimal change that keeps existing functional behavior (log and return false on permission-check errors) while addressing the “generic catch” issue is:
- Replace
catch (Exception ex)with one or more specific catches for expected exceptions, in particularHttpRequestException. - Optionally add separate branches for 401/403, analogous to
CheckAuthorizationAsync, while still returningfalse(so callers see the same boolean behavior). - Let other unexpected exceptions propagate (no generic catch) so they can be diagnosed correctly instead of being silently converted into
false.
Concretely, in HasPermissionAsync (around lines 101–112) we will:
- Replace the single
catch (Exception ex)with:catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Forbidden)catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)catch (HttpRequestException ex)for other HTTP issues
- In all these branches, log the error and return
falseas before. - Do not introduce a new generic
catch (Exception)so we satisfy the CodeQL rule.
System.Net and System.Net.Http are already imported at the top of the file, so no new imports are required.
| @@ -105,11 +105,21 @@ | ||
| var userPermissions = await PermissionsService.GetUserPermissions(); | ||
| return userPermissions.Contains(permission); | ||
| } | ||
| catch (Exception ex) | ||
| catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Forbidden) | ||
| { | ||
| Logger.LogError(ex, "Error checking permission {Permission}", permission); | ||
| Logger.LogWarning(ex, "Access forbidden when checking permission {Permission}", permission); | ||
| return false; | ||
| } | ||
| catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized) | ||
| { | ||
| Logger.LogWarning(ex, "User unauthorized when checking permission {Permission}", permission); | ||
| return false; | ||
| } | ||
| catch (HttpRequestException ex) | ||
| { | ||
| Logger.LogError(ex, "HTTP error when checking permission {Permission}", permission); | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
| } |
| catch (Exception ex) | ||
| { | ||
| // Log the error but set unauthorized to be safe | ||
| Logger.LogError(ex, "Error checking user permissions"); | ||
| IsAuthorized = false; | ||
| } |
Check notice
Code scanning / CodeQL
Generic catch clause Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 months ago
In general, to fix a generic catch clause, replace catch (Exception) with one or more catch blocks for specific, expected exception types, and avoid swallowing unexpected exceptions. If there is a need for a last‑resort catch, it should normally rethrow after logging, so that real bugs are not silently masked.
In this file, the main risk is in CheckAuthorizationAsync, where catch (Exception ex) logs the error and simply sets IsAuthorized = false. A better approach is:
- Keep the existing specific
HttpRequestExceptionfilters forForbiddenandUnauthorized. - Add another
catch (HttpRequestException ex)without a filter to handle other HTTP‑related errors (network issues, 5xx, etc.) and treat them as “not authorized but with error”, still logging the details. - Remove the generic
catch (Exception ex)fromCheckAuthorizationAsyncso unexpected exceptions are not silently converted into authorization failures and can surface appropriately.
For HasPermissionAsync, the behaviour of returning false for unexpected failures might be intentional (permissions checks often choose “fail closed”), and this method is narrow in scope. Leaving its generic catch in place maintains existing functionality. The CodeQL report is about line 85, which is in CheckAuthorizationAsync, so we only change that block.
Concretely, in src/IoTHub.Portal.Client/Components/Commons/AuthorizedComponentBase.cs:
- Replace the final
catch (Exception ex)inCheckAuthorizationAsyncwith acatch (HttpRequestException ex)that logs with more context (e.g., includingStatusCode) and setsIsAuthorized = false. - Do not change the method signature, existing logging setup, or behaviour of other methods.
- No new imports are required;
HttpRequestExceptionandHttpStatusCodeare already in use.
| @@ -82,10 +82,10 @@ | ||
| Logger.LogWarning("User unauthorized when checking permissions"); | ||
| IsAuthorized = false; | ||
| } | ||
| catch (Exception ex) | ||
| catch (HttpRequestException ex) | ||
| { | ||
| // Log the error but set unauthorized to be safe | ||
| Logger.LogError(ex, "Error checking user permissions"); | ||
| // Log other HTTP-related errors and set unauthorized to be safe | ||
| Logger.LogError(ex, "HTTP error when checking user permissions. Status code: {StatusCode}", ex.StatusCode); | ||
| IsAuthorized = false; | ||
| } | ||
| finally |
| foreach (var ac in accessControls) | ||
| { | ||
| if (ac.Role?.Actions != null && | ||
| ac.Role.Actions.Any(a => string.Equals(a.Name, permission, StringComparison.OrdinalIgnoreCase))) | ||
| { | ||
| return true; | ||
| } | ||
| } |
Check notice
Code scanning / CodeQL
Missed opportunity to use Where Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 months ago
In general, to fix a missed Where opportunity in a foreach, move the filtering logic that appears as an if guard inside the loop into a LINQ Where clause (or even directly into Any / All / FirstOrDefault etc., if the loop is only used for a boolean or search outcome). This reduces nesting, makes the predicate explicit, and improves readability.
For this specific method, UserHasPermissionAsync only needs to know whether any access control has a role whose actions contain an entry matching the requested permission (case-insensitive). Instead of looping and returning true from inside the loop, we can compute a boolean with a single LINQ Any call. Concretely, replace the foreach block (lines 178–185) and the subsequent return false (line 186) with a single statement:
return accessControls.Any(ac =>
ac.Role?.Actions != null &&
ac.Role.Actions.Any(a => string.Equals(a.Name, permission, StringComparison.OrdinalIgnoreCase)));This preserves the logic exactly:
- It only returns
trueif there exists at least oneacfor whichac.Role?.Actions != nulland at least one action name equalspermissionignoring case. - It returns
falseotherwise, becauseAnyreturnsfalsewhen no element satisfies the predicate.
No extra imports are needed: System.Linq is already imported and Any is part of LINQ. All changes are confined to UserHasPermissionAsync in src/IoTHub.Portal.Application/Services/AccessControlService.cs.
| @@ -175,15 +175,9 @@ | ||
| ); | ||
|
|
||
| // For each access control, check if the associated role contains an action matching the requested permission. | ||
| foreach (var ac in accessControls) | ||
| { | ||
| if (ac.Role?.Actions != null && | ||
| ac.Role.Actions.Any(a => string.Equals(a.Name, permission, StringComparison.OrdinalIgnoreCase))) | ||
| { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| return accessControls.Any(ac => | ||
| ac.Role?.Actions != null && | ||
| ac.Role.Actions.Any(a => string.Equals(a.Name, permission, StringComparison.OrdinalIgnoreCase))); | ||
| } | ||
| } | ||
| } |
…nformation Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
| } | ||
| } | ||
|
|
||
| this.logger.LogInformation("User with principal ID {PrincipalId} has {Count} permissions", user.PrincipalId, userPermissions.Count); |
Check warning
Code scanning / CodeQL
Exposure of private information Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 months ago
In general, the problem is that potentially sensitive user-specific data (user.PrincipalId) is being written to logs, which are an external storage location that may not have the same access controls as runtime data. The fix is to avoid logging sensitive identifiers directly, or to log a less sensitive surrogate (for example, a stable but non-reversible hash or a generic message without the identifier).
The minimal, non-breaking change that best addresses this is to modify the log statement on line 78 to remove user.PrincipalId from the structured log parameters. We can still keep useful diagnostic information (such as the count of permissions) without including a unique identifier for the user. Given the constraints, we should not introduce new crypto helpers or complex hashing logic; the simplest and safest approach is to log only non-sensitive information. Concretely, in GetMyPermissions, replace:
this.logger.LogInformation("User with principal ID {PrincipalId} has {Count} permissions", user.PrincipalId, userPermissions.Count);with something like:
this.logger.LogInformation("Current authenticated user has {Count} permissions", userPermissions.Count);This preserves logging of authorization behavior without exposing the principal ID. No new imports, methods, or definitions are needed; only the one log line in PermissionsController.GetMyPermissions needs to be changed.
| @@ -75,7 +75,7 @@ | ||
| } | ||
| } | ||
|
|
||
| this.logger.LogInformation("User with principal ID {PrincipalId} has {Count} permissions", user.PrincipalId, userPermissions.Count); | ||
| this.logger.LogInformation("Current authenticated user has {Count} permissions", userPermissions.Count); | ||
|
|
||
| return Ok(userPermissions.ToArray()); | ||
| } |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #3284 +/- ##
==========================================
- Coverage 82.92% 81.04% -1.89%
==========================================
Files 372 375 +3
Lines 13752 14195 +443
Branches 1072 1185 +113
==========================================
+ Hits 11404 11504 +100
- Misses 2019 2353 +334
- Partials 329 338 +9 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Description
What kind of change does this PR introduce?