Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions src/Dockerfile.debug
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,31 @@ FROM docker.artifactory.michelin.com/dotnet/sdk:8.0 AS build
ARG BUILD_VERSION=1.2.3
ARG GITHUB_RUN_NUMBER=4
WORKDIR /src
RUN ls

COPY nuget.config .
COPY ["IoTHub.Portal.Application/IoTHub.Portal.Application.csproj", "IoTHub.Portal.Application/"]
COPY ["IoTHub.Portal.Client/IoTHub.Portal.Client.csproj", "IoTHub.Portal.Client/"]
COPY ["IoTHub.Portal.Crosscutting/IoTHub.Portal.Crosscutting.csproj", "IoTHub.Portal.Crosscutting/"]
COPY ["IoTHub.Portal.Domain/IoTHub.Portal.Domain.csproj", "IoTHub.Portal.Domain/"]
COPY ["IoTHub.Portal.Infrastructure/IoTHub.Portal.Infrastructure.csproj", "IoTHub.Portal.Infrastructure/"]
COPY ["IoTHub.Portal.MySql/IoTHub.Portal.MySql.csproj", "IoTHub.Portal.MySql/"]
COPY ["IoTHub.Portal.Postgres/IoTHub.Portal.Postgres.csproj", "IoTHub.Portal.Postgres/"]
COPY ["IoTHub.Portal.Server/IoTHub.Portal.Server.csproj", "IoTHub.Portal.Server/"]
COPY ["IoTHub.Portal.Shared/IoTHub.Portal.Shared.csproj", "IoTHub.Portal.Shared/"]
COPY ["IoTHub.Portal.Client/IoTHub.Portal.Client.csproj", "IoTHub.Portal.Client/"]

RUN dotnet restore "IoTHub.Portal.Application/IoTHub.Portal.Application.csproj"
RUN dotnet restore "IoTHub.Portal.Client/IoTHub.Portal.Client.csproj"
RUN dotnet restore "IoTHub.Portal.Crosscutting/IoTHub.Portal.Crosscutting.csproj"
RUN dotnet restore "IoTHub.Portal.Domain/IoTHub.Portal.Domain.csproj"
RUN dotnet restore "IoTHub.Portal.Server/IoTHub.Portal.Server.csproj"
RUN dotnet restore "IoTHub.Portal.Shared/IoTHub.Portal.Shared.csproj"

COPY . .
WORKDIR "/src/IoTHub.Portal.Server"
RUN dotnet build "IoTHub.Portal.Server.csproj" -c Debug -o /app/build -p:Version="${BUILD_VERSION}.${GITHUB_RUN_NUMBER}"

FROM build AS publish
ARG BUILD_VERSION=1.2.3
ARG GITHUB_RUN_NUMBER=4
RUN dotnet publish "IoTHub.Portal.Server.csproj" -c Debug -o /app/publish -p:Version="${BUILD_VERSION}.${GITHUB_RUN_NUMBER}"

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "IoTHub.Portal.Server.dll"]
42 changes: 0 additions & 42 deletions src/IoTHub.Portal.Application/Mappers/GroupProfile.cs

This file was deleted.

1 change: 0 additions & 1 deletion src/IoTHub.Portal.Application/Mappers/UserProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ public UserProfile()
.ForMember(dest => dest.PrincipalId, opts => opts.MapFrom(src => src.PrincipalId));

_ = CreateMap<UserDetailsModel, User>()
.ForMember(dest => dest.Id, opt => opt.Ignore())
.ForMember(dest => dest.Id, opt => opt.Ignore())
.ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Email))
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name))
Expand Down
36 changes: 35 additions & 1 deletion src/IoTHub.Portal.Application/Services/AccessControlService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
using IoTHub.Portal.Shared.Models.v10.Filters;
using IoTHub.Portal.Domain.Exceptions;
using IoTHub.Portal.Crosscutting;
using System.Linq; // for Select
using System.Linq.Expressions; // for includes expressions

public class AccessControlService : IAccessControlManagementService
{
Expand Down Expand Up @@ -67,11 +69,13 @@
acPredicate = acPredicate.And(ac => ac.PrincipalId == principalId);
}

// Include Role so that Role.Name is populated for UI display
var paginatedAc = await this.accessControlRepository.GetPaginatedListAsync(
pageNumber,
pageSize,
orderBy,
acPredicate
acPredicate,
includes: new Expression<System.Func<AccessControl, object>>[] { ac => ac.Role }
);

var paginatedAcDto = new PaginatedResult<AccessControlModel>
Expand Down Expand Up @@ -151,5 +155,35 @@
await this.unitOfWork.SaveAsync();
return true;
}

/// <summary>
/// Verify if the principal (user) has the specified permission.
/// For this, we retrieve all access controls related to the principal including the role and its actions,
/// then we check if one of the role's actions matches (case insensitive) the requested permission.
/// </summary>
/// <param name="permission">Permission name (e.g. "group:read").</param>
/// <param name="principalId">Principal identifier of the user (trying to reach the request) </param>
/// <returns> True if the permission is found, else False </returns>
public async Task<bool> UserHasPermissionAsync(string principalId, string permission)
{
// We retrieve the access controls of the principal including the role and its list of actions.
var accessControls = await this.accessControlRepository.GetAllAsync(
ac => ac.PrincipalId == principalId,
CancellationToken.None,
ac => ac.Role,
ac => ac.Role.Actions
);

// 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;
}
}
Comment on lines +178 to +185

Check notice

Code scanning / CodeQL

Missed opportunity to use Where Note

This foreach loop
implicitly filters its target sequence
- consider filtering the sequence explicitly using '.Where(...)'.

Copilot Autofix

AI 7 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 true if there exists at least one ac for which ac.Role?.Actions != null and at least one action name equals permission ignoring case.
  • It returns false otherwise, because Any returns false when 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.


Suggested changeset 1
src/IoTHub.Portal.Application/Services/AccessControlService.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/IoTHub.Portal.Application/Services/AccessControlService.cs b/src/IoTHub.Portal.Application/Services/AccessControlService.cs
--- a/src/IoTHub.Portal.Application/Services/AccessControlService.cs
+++ b/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)));
         }
     }
 }
EOF
@@ -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)));
}
}
}
Copilot is powered by AI and may make mistakes. Always verify output.
return false;
}
}
}
173 changes: 0 additions & 173 deletions src/IoTHub.Portal.Application/Services/GroupService.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,7 @@ Task<PaginatedResult<AccessControlModel>> GetAccessControlPage(
Task<AccessControlModel> CreateAccessControl(AccessControlModel role);
Task<AccessControlModel?> UpdateAccessControl(string id, AccessControlModel accessControl);
Task<bool> DeleteAccessControl(string id);

Task<bool> UserHasPermissionAsync(string principalId, string permission);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace IoTHub.Portal.Application.Services
{
using IoTHub.Portal.Shared.Models.v10;
using System.Security.Claims;
using IoTHub.Portal.Shared.Models.v10;

public interface IUserManagementService
Expand All @@ -19,6 +19,6 @@ Task<PaginatedResult<UserModel>> GetUserPage(
Task<UserDetailsModel> CreateUserAsync(UserDetailsModel userCreateModel);
Task<UserDetailsModel?> UpdateUser(string id, UserDetailsModel user);
Task<bool> DeleteUser(string userId);

Task<UserDetailsModel> GetOrCreateUserByEmailAsync(string email, ClaimsPrincipal principal);
}
}
Loading
Loading