Add instructor rating feature with enrollment-gated reviews#71
Add instructor rating feature with enrollment-gated reviews#71
Conversation
…s, and API Co-authored-by: kavyashri-as <213833080+kavyashri-as@users.noreply.github.com> Agent-Logs-Url: https://github.com/CanarysPlayground/CourseApplication/sessions/e5aba3b0-3bdb-4857-9b2a-57cfffb98068
| [ProducesResponseType(typeof(ApiResponseDto<IEnumerable<InstructorRatingDto>>), StatusCodes.Status200OK)] | ||
| public async Task<ActionResult<ApiResponseDto<IEnumerable<InstructorRatingDto>>>> GetRatingsByInstructor(string instructorName) | ||
| { | ||
| _logger.LogInformation("Getting ratings for instructor: {InstructorName}", instructorName); |
Check failure
Code scanning / CodeQL
Log entries created from user input High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 2 days ago
In general, to fix log-forging issues when logging user input, normalize or encode the input before logging. For plain-text logs, remove newline and carriage return characters (and optionally other control characters). For HTML logs, HTML-encode the value. The aim is to ensure a malicious user cannot inject extra log lines or formatting.
In this case, the best fix is to sanitize instructorName just before it is passed to the logger, by removing newline (\n) and carriage return (\r) characters. This keeps all existing functionality intact (the service call still receives the original, unsanitized instructorName for correct lookups) while ensuring that the logged version cannot create forged entries. We only need to change the body of GetRatingsByInstructor in api/CourseRegistration.API/Controllers/InstructorRatingsController.cs: introduce a local variable that holds a sanitized copy for logging and use that in _logger.LogInformation(...). No new imports are required; we can rely on string.Replace which is in System and already available.
| @@ -113,7 +113,12 @@ | ||
| [ProducesResponseType(typeof(ApiResponseDto<IEnumerable<InstructorRatingDto>>), StatusCodes.Status200OK)] | ||
| public async Task<ActionResult<ApiResponseDto<IEnumerable<InstructorRatingDto>>>> GetRatingsByInstructor(string instructorName) | ||
| { | ||
| _logger.LogInformation("Getting ratings for instructor: {InstructorName}", instructorName); | ||
| var safeInstructorName = instructorName? | ||
| .Replace("\r\n", string.Empty) | ||
| .Replace("\n", string.Empty) | ||
| .Replace("\r", string.Empty); | ||
|
|
||
| _logger.LogInformation("Getting ratings for instructor: {InstructorName}", safeInstructorName); | ||
|
|
||
| var ratings = await _ratingService.GetRatingsByInstructorNameAsync(instructorName); | ||
| return Ok(ApiResponseDto<IEnumerable<InstructorRatingDto>>.SuccessResponse(ratings, "Ratings retrieved successfully")); |
| [ProducesResponseType(typeof(ApiResponseDto<object>), StatusCodes.Status404NotFound)] | ||
| public async Task<ActionResult<ApiResponseDto<InstructorRatingStatsDto>>> GetInstructorStats(string instructorName) | ||
| { | ||
| _logger.LogInformation("Getting stats for instructor: {InstructorName}", instructorName); |
Check failure
Code scanning / CodeQL
Log entries created from user input High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 2 days ago
To fix this, we should sanitize the user-provided instructorName before it is logged, ensuring that any newline or carriage-return characters (and optionally other control characters) are removed. This prevents a malicious user from injecting extra log lines or confusing content into plain text logs while preserving the semantic value of the log entry.
The most direct, low-impact fix is to introduce a local, sanitized variable inside GetInstructorStats (and, for consistency and to eliminate a similar pattern, in GetRatingsByInstructor as well) and use that variable in the _logger.LogInformation calls. For example, we can call Replace("\r", "").Replace("\n", "") on instructorName. This keeps the method signature and its behavior unchanged for the rest of the system while ensuring that what is written to the logs cannot contain line breaks. No new imports are required; we use string’s built-in methods.
Concretely:
- In
GetRatingsByInstructor, before logging, definevar sanitizedInstructorName = instructorName.Replace("\r", string.Empty).Replace("\n", string.Empty);and use that in the_logger.LogInformationcall. - In
GetInstructorStats(line 130+), likewise define asanitizedInstructorNamevariable and use it in the log call on line 132, leaving the rest of the logic intact.
| @@ -113,7 +113,8 @@ | ||
| [ProducesResponseType(typeof(ApiResponseDto<IEnumerable<InstructorRatingDto>>), StatusCodes.Status200OK)] | ||
| public async Task<ActionResult<ApiResponseDto<IEnumerable<InstructorRatingDto>>>> GetRatingsByInstructor(string instructorName) | ||
| { | ||
| _logger.LogInformation("Getting ratings for instructor: {InstructorName}", instructorName); | ||
| var sanitizedInstructorName = instructorName.Replace("\r", string.Empty).Replace("\n", string.Empty); | ||
| _logger.LogInformation("Getting ratings for instructor: {InstructorName}", sanitizedInstructorName); | ||
|
|
||
| var ratings = await _ratingService.GetRatingsByInstructorNameAsync(instructorName); | ||
| return Ok(ApiResponseDto<IEnumerable<InstructorRatingDto>>.SuccessResponse(ratings, "Ratings retrieved successfully")); | ||
| @@ -129,7 +130,8 @@ | ||
| [ProducesResponseType(typeof(ApiResponseDto<object>), StatusCodes.Status404NotFound)] | ||
| public async Task<ActionResult<ApiResponseDto<InstructorRatingStatsDto>>> GetInstructorStats(string instructorName) | ||
| { | ||
| _logger.LogInformation("Getting stats for instructor: {InstructorName}", instructorName); | ||
| var sanitizedInstructorName = instructorName.Replace("\r", string.Empty).Replace("\n", string.Empty); | ||
| _logger.LogInformation("Getting stats for instructor: {InstructorName}", sanitizedInstructorName); | ||
|
|
||
| var stats = await _ratingService.GetInstructorStatsAsync(instructorName); | ||
| if (stats == null) |
| [HttpDelete("{id:guid}")] | ||
| [ProducesResponseType(typeof(ApiResponseDto<object>), StatusCodes.Status200OK)] | ||
| [ProducesResponseType(typeof(ApiResponseDto<object>), StatusCodes.Status404NotFound)] | ||
| public async Task<ActionResult<ApiResponseDto<object>>> DeleteRating(Guid id) |
Check failure
Code scanning / CodeQL
Missing function level access control High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 2 days ago
In general, sensitive controller actions should be protected by explicit authorization checks. For ASP.NET Core Web API, this is most commonly done using the [Authorize] attribute (optionally with roles/policies) so that only authenticated and authorized users can call these endpoints. If finer-grained control is required, additional checks can be done in the action method body (e.g., verifying that the current user is the owner of the resource or has a specific role).
The single best fix here, without changing existing functionality of the service layer, is to decorate the DeleteRating action (and, for consistency, the similarly sensitive UpdateRating action) with an [Authorize] attribute so that only authenticated users can update or delete ratings. This introduces a clear, declarative access control requirement on these methods while preserving their existing logic and return types. To implement this, we need to import Microsoft.AspNetCore.Authorization at the top of the file and add [Authorize] above the DeleteRating method. Given the similar risk profile, adding [Authorize] above UpdateRating in the same file is also recommended and consistent with the reported issue type.
Concretely in api/CourseRegistration.API/Controllers/InstructorRatingsController.cs:
- Add
using Microsoft.AspNetCore.Authorization;alongside the existingusingstatements. - Add
[Authorize]above theUpdateRatingmethod. - Add
[Authorize]above theDeleteRatingmethod.
No other code changes or new methods are required.
| @@ -1,6 +1,7 @@ | ||
| using Microsoft.AspNetCore.Mvc; | ||
| using CourseRegistration.Application.DTOs; | ||
| using CourseRegistration.Application.Interfaces; | ||
| using Microsoft.AspNetCore.Authorization; | ||
|
|
||
| namespace CourseRegistration.API.Controllers; | ||
|
|
||
| @@ -169,6 +170,7 @@ | ||
| /// <param name="updateRatingDto">Rating update details</param> | ||
| /// <returns>Updated rating</returns> | ||
| [HttpPut("{id:guid}")] | ||
| [Authorize] | ||
| [ProducesResponseType(typeof(ApiResponseDto<InstructorRatingDto>), StatusCodes.Status200OK)] | ||
| [ProducesResponseType(typeof(ApiResponseDto<object>), StatusCodes.Status404NotFound)] | ||
| public async Task<ActionResult<ApiResponseDto<InstructorRatingDto>>> UpdateRating(Guid id, [FromBody] UpdateInstructorRatingDto updateRatingDto) | ||
| @@ -190,6 +192,7 @@ | ||
| /// <param name="id">Rating ID</param> | ||
| /// <returns>Success status</returns> | ||
| [HttpDelete("{id:guid}")] | ||
| [Authorize] | ||
| [ProducesResponseType(typeof(ApiResponseDto<object>), StatusCodes.Status200OK)] | ||
| [ProducesResponseType(typeof(ApiResponseDto<object>), StatusCodes.Status404NotFound)] | ||
| public async Task<ActionResult<ApiResponseDto<object>>> DeleteRating(Guid id) |
| [HttpDelete("{id:guid}")] | ||
| [ProducesResponseType(typeof(ApiResponseDto<object>), StatusCodes.Status200OK)] | ||
| [ProducesResponseType(typeof(ApiResponseDto<object>), StatusCodes.Status404NotFound)] | ||
| public async Task<ActionResult<ApiResponseDto<object>>> DeleteRating(Guid id) |
Check failure
Code scanning / CodeQL
Insecure Direct Object Reference High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 2 days ago
In general, to fix IDOR issues you must ensure that operations on an object identified by a client‑supplied ID are only allowed if the current user is authorized to access that specific object. In ASP.NET Core this is typically done by (a) retrieving the resource, (b) running an authorization check against the current User (e.g., via IAuthorizationService or a policy), and only then (c) performing the update/delete if authorized, otherwise returning Forbid() or Unauthorized().
For this controller, the least disruptive fix that keeps existing functionality is:
- Inject
IAuthorizationServiceintoInstructorRatingsController. - Before calling the service methods that update or delete a rating (
UpdateRatingandDeleteRating), fetch the rating by ID from the service. - If the rating does not exist, keep returning 404 as today.
- If it exists, call
_authorizationService.AuthorizeAsync(User, rating, "InstructorRatingOwnerPolicy")(or similar) to ensure the current user is allowed to act on that rating.- On success, proceed with update/delete as currently.
- On failure, return
Forbid()wrapped in the sameApiResponseDto<object>/ApiResponseDto<InstructorRatingDto>style used elsewhere, to avoid changing the API contract.
Because we only see part of the file, the code changes must stay inside the shown controller. Concretely:
- Add a
using Microsoft.AspNetCore.Authorization;import at the top so we can useIAuthorizationService. - Add a private readonly field
_authorizationServiceand update the controller constructor (in the shown region) to accept and assign it. - In
UpdateRating, first call something likevar existingRating = await _ratingService.GetRatingByIdAsync(id);(assuming such a method exists; if not, use whatever retrieval method is available in the shown snippet) and:- If
existingRating == null, return 404 as today. - Otherwise, call
_authorizationService.AuthorizeAsync(User, existingRating, "InstructorRatingOwnerPolicy"). - If not authorized, return
Forbid(ApiResponseDto<InstructorRatingDto>.ErrorResponse("Not authorized to update this rating")). - Only then call
_ratingService.UpdateRatingAsync(id, updateRatingDto)and return success.
- If
- In
DeleteRating, do the same pattern: retrieve rating, check authorization, then call_ratingService.DeleteRatingAsync(id).
Since we must not assume new service methods that aren’t shown, you should adapt the retrieval to whatever method exists in the real file (e.g., GetRatingByIdAsync). In the replacement blocks below I’ll illustrate using a plausible GetRatingByIdAsync that would be present in the same service, but you may need to align the exact method name/signature with the rest of your code.
| @@ -1,6 +1,7 @@ | ||
| using Microsoft.AspNetCore.Mvc; | ||
| using CourseRegistration.Application.DTOs; | ||
| using CourseRegistration.Application.Interfaces; | ||
| using Microsoft.AspNetCore.Authorization; | ||
|
|
||
| namespace CourseRegistration.API.Controllers; | ||
|
|
||
| @@ -13,6 +14,7 @@ | ||
| public class InstructorRatingsController : ControllerBase | ||
| { | ||
| private readonly IInstructorRatingService _ratingService; | ||
| private readonly IAuthorizationService _authorizationService; | ||
| private readonly ILogger<InstructorRatingsController> _logger; | ||
|
|
||
| /// <summary> | ||
| @@ -174,10 +176,24 @@ | ||
| public async Task<ActionResult<ApiResponseDto<InstructorRatingDto>>> UpdateRating(Guid id, [FromBody] UpdateInstructorRatingDto updateRatingDto) | ||
| { | ||
| _logger.LogInformation("Updating rating with ID: {RatingId}", id); | ||
|
|
||
| // Retrieve existing rating to validate authorization and existence | ||
| var existingRating = await _ratingService.GetRatingByIdAsync(id); | ||
| if (existingRating == null) | ||
| { | ||
| return NotFound(ApiResponseDto<InstructorRatingDto>.ErrorResponse("Rating not found")); | ||
| } | ||
|
|
||
| var authorizationResult = await _authorizationService.AuthorizeAsync(User, existingRating, "InstructorRatingOwnerPolicy"); | ||
| if (!authorizationResult.Succeeded) | ||
| { | ||
| return Forbid(ApiResponseDto<InstructorRatingDto>.ErrorResponse("Not authorized to update this rating")); | ||
| } | ||
|
|
||
| var rating = await _ratingService.UpdateRatingAsync(id, updateRatingDto); | ||
| if (rating == null) | ||
| { | ||
| // In case the rating was deleted or became unavailable during update | ||
| return NotFound(ApiResponseDto<InstructorRatingDto>.ErrorResponse("Rating not found")); | ||
| } | ||
|
|
||
| @@ -195,6 +207,19 @@ | ||
| public async Task<ActionResult<ApiResponseDto<object>>> DeleteRating(Guid id) | ||
| { | ||
| _logger.LogInformation("Deleting rating with ID: {RatingId}", id); | ||
|
|
||
| // Retrieve existing rating to validate authorization and existence | ||
| var existingRating = await _ratingService.GetRatingByIdAsync(id); | ||
| if (existingRating == null) | ||
| { | ||
| return NotFound(ApiResponseDto<object>.ErrorResponse("Rating not found")); | ||
| } | ||
|
|
||
| var authorizationResult = await _authorizationService.AuthorizeAsync(User, existingRating, "InstructorRatingOwnerPolicy"); | ||
| if (!authorizationResult.Succeeded) | ||
| { | ||
| return Forbid(ApiResponseDto<object>.ErrorResponse("Not authorized to delete this rating")); | ||
| } | ||
|
|
||
| var success = await _ratingService.DeleteRatingAsync(id); | ||
| if (!success) |
Co-authored-by: kavyashri-as <213833080+kavyashri-as@users.noreply.github.com> Agent-Logs-Url: https://github.com/CanarysPlayground/CourseApplication/sessions/e5aba3b0-3bdb-4857-9b2a-57cfffb98068
…N+1 queries Co-authored-by: kavyashri-as <213833080+kavyashri-as@users.noreply.github.com> Agent-Logs-Url: https://github.com/CanarysPlayground/CourseApplication/sessions/e5aba3b0-3bdb-4857-9b2a-57cfffb98068
Implements a rating system allowing enrolled students to review instructors on a 1-5 scale with optional comments.
Core Components
InstructorRatingwith unique constraint on(StudentId, CourseId)to prevent duplicate ratingsIInstructorRatingRepositorywith queries by course, student, and instructor nameBusiness Rules
API Surface
POST /api/instructorratings- Create rating (validates enrollment)GET /api/instructorratings/instructor/{name}/stats- Aggregate statistics with distribution breakdownGET /api/instructorratings/course/{courseId}- All ratings for a courseDTOs
CreateInstructorRatingDto- Rating (1-5), CourseId, StudentId, optional CommentInstructorRatingStatsDto- AverageRating, TotalRatings, star distribution (1-5), CourseIdsStatistics are calculated in-memory from repository queries; no precomputed aggregates.
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.