Skip to content

Feature/userinfo - #273

Open
JasonRobertFrancis wants to merge 54 commits into
mainfrom
feature/userinfo
Open

Feature/userinfo#273
JasonRobertFrancis wants to merge 54 commits into
mainfrom
feature/userinfo

Conversation

@JasonRobertFrancis

Copy link
Copy Markdown
Contributor

No description provided.

@codecov-commenter

codecov-commenter commented Jul 28, 2026

Copy link
Copy Markdown

Bundle Report

Bundle size has no change ✅

@codecov-commenter

codecov-commenter commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 11.51132% with 6019 lines in your changes missing coverage. Please review.
✅ Project coverage is 38.56%. Comparing base (ee1cfed) to head (c18d9c8).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
web/Areas/Directory/Services/UserInfoService.cs 53.34% 480 Missing and 43 partials ⚠️
web/Areas/Directory/Views/UserInfo.cshtml 0.00% 426 Missing ⚠️
web/Models/PPS/VwUserinfoUser.cs 0.00% 149 Missing ⚠️
web/Models/PPS/PsJobV.cs 0.00% 125 Missing ⚠️
web/Models/PPS/JobDV.cs 0.00% 121 Missing ⚠️
web/Models/PPS/JpmJpItemDV.cs 0.00% 121 Missing ⚠️
web/Models/PPS/PositionDV.cs 0.00% 118 Missing ⚠️
web/Models/PPS/PsJpmJpItemsV.cs 0.00% 116 Missing ⚠️
web/Models/PPS/EdbperVc.cs 0.00% 104 Missing ⚠️
web/Models/PPS/VwAllJobPosOrg.cs 0.00% 102 Missing ⚠️
... and 191 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #273      +/-   ##
==========================================
- Coverage   41.74%   38.56%   -3.19%     
==========================================
  Files         992     1184     +192     
  Lines       49697    56490    +6793     
  Branches     5854     6129     +275     
==========================================
+ Hits        20748    21785    +1037     
- Misses      28038    33721    +5683     
- Partials      911      984      +73     
Flag Coverage Δ
backend 36.54% <11.51%> (-3.26%) ⬇️
frontend 58.15% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
web/Areas/Directory/Models/IDCardResult.cs 100.00% <100.00%> (ø)
web/Areas/Directory/Models/InstinctResult.cs 100.00% <100.00%> (ø)
web/Areas/Directory/Models/LoanResult.cs 100.00% <100.00%> (ø)
.../Directory/Models/IndividualSearchResultWithIDs.cs 0.00% <0.00%> (ø)
web/Areas/Directory/Models/KeyResult.cs 83.33% <83.33%> (ø)
web/Areas/Directory/Models/LdapUserContact.cs 0.00% <0.00%> (ø)
web/Models/IDCards/DvtCardStatus.cs 80.00% <80.00%> (ø)
web/Areas/RAPS/Services/UinformService.cs 21.76% <86.66%> (+18.96%) ⬆️
web/Models/EquipmentLoan/AppSetting.cs 0.00% <0.00%> (ø)
web/Models/EquipmentLoan/AssetType.cs 0.00% <0.00%> (ø)
... and 194 more

... and 15 files with indirect coverage changes

Comment thread test/Services/UserInfoServiceUnitTests.cs Fixed
Comment thread web/Areas/Directory/Services/UserInfoService.cs Fixed
Comment thread web/Areas/Directory/Services/UserInfoService.cs Fixed
Comment thread web/Areas/Directory/Services/UserInfoService.cs Fixed
Comment thread web/Areas/Directory/Services/UserInfoService.cs Fixed
Comment thread web/Classes/Utilities/IamApi.cs Fixed
Comment thread web/Areas/Directory/Services/UserInfoService.cs Fixed
Comment thread web/Areas/Directory/Services/UserInfoService.cs Fixed
Comment thread web/Areas/Directory/Services/UserInfoService.cs Fixed
Comment thread web/Areas/Directory/Views/UserInfo.cshtml Fixed

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

Comment thread web/Areas/Directory/Services/UserInfoService.cs Fixed
Comment thread web/Areas/Directory/Services/UserInfoService.cs Fixed
Comment thread web/Classes/Utilities/IamApi.cs Fixed
@rlorenzo

This comment was marked as resolved.

Comment thread web/Areas/Directory/Services/UserInfoService.cs Fixed
Comment thread web/Areas/Directory/Services/UserInfoService.cs Fixed
Comment thread web/Areas/Directory/Services/UserInfoService.cs Fixed
@rlorenzo

This comment was marked as resolved.

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 218 out of 220 changed files in this pull request and generated no new comments.

Suppressed comments (3)

web/Areas/Directory/Controllers/DirectoryController.cs:145

  • This refactor dropped the length guard that the original inline code (and the comment just below in AddVmacsContactInfoAsync) relied on. Nextel, LDPager, and Unit are string[]?, and empty XML element lists deserialize as empty (length‑0) arrays rather than null. With only a != null check, indexing [0] on an empty array will throw IndexOutOfRangeException. Restore the { Length: > 0 } guard (consistent with UserInfoService.cs which uses ?.Length > 0).
                if (vm.item.Nextel != null) result.Nextel = vm.item.Nextel[0];
                if (vm.item.LDPager != null) result.LDPager = vm.item.LDPager[0];
                if (vm.item.Unit != null) result.Department = vm.item.Unit[0];

web/Areas/Directory/Models/IndividualSearchResultWithIDs.cs:49

  • LdapUserContact.PostalAddress is declared = null! and is only assigned when the LDAP entry contains a postalAddress attribute, so it can be null at runtime. Removing the null guard here means PostalAddress.Replace(...) can throw a NullReferenceException. Note the base IndividualSearchResult constructor already uses the null-safe form (?.Replace(...) ?? ""); this override should match it.
                PostalAddress = ldapUserContact.PostalAddress.Replace("$", '\n'.ToString());

web/Classes/Utilities/LdapService.cs:123

  • The param documentation is inaccurate: this method looks up by MothraID (the filter uses ucdpersonuuid, which maps to LdapUserContact.MothraId), not by iamID. Describing the parameter as "iamID" could lead callers to pass the wrong identifier.

Comment thread web/Areas/Directory/Services/UserInfoService.cs Fixed
Comment thread web/Areas/Directory/Controllers/DirectoryController.cs Outdated
Comment thread web/Areas/Directory/Services/UserInfoService.cs Outdated
Comment thread web/Areas/Directory/Services/UserInfoService.cs Fixed
Comment thread web/Areas/Directory/Services/UserInfoService.cs Fixed
Comment thread web/Areas/Directory/Services/UserInfoService.cs Fixed
Comment thread web/Viper.csproj
<ImplicitUsings>enable</ImplicitUsings>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<PublishDir>bin\Release\net10.0\publish\</PublishDir>
<NoWarn>$(NoWarn);NU1902;NU1608</NoWarn>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@JasonRobertFrancis Suppressing NU1902 silences future NuGet vulnerability advisories for the whole project, please drop it.

}
@if (!string.IsNullOrEmpty(Model.PostalAddress))
{
<li><strong>Address:</strong> @Html.Raw(Model.PostalAddress)</li>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@JasonRobertFrancis Html.Raw on LDAP data opens a possible XSS vector here. Encode before substituting the delimiter:

@Html.Raw(Html.Encode(Model.PostalAddress).Replace("$", "<br>"))

and drop the .Replace at UserInfoService.cs:267. Line 151 needs no raw at all, StudentPriorName contains no markup.


try
{
var uinformService = new UinformService();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@JasonRobertFrancis The unit tests are calling a live API service. UinformService uses its own static HttpClient with a hardcoded host, so the mocked IHttpClientFactory is bypassed and both GetUserInfoAsync_* tests hit ws.uinform-test.ucdavis.edu on every run. Line 260 does the same to ldap.ucdavis.edu:636 via LdapService. The catch filters hide it, which is why they still pass. Either inject these behind interfaces or give UinformService the same "skip when unconfigured" guard VMACSService has.

@bsedwards

Copy link
Copy Markdown
Collaborator

@JasonRobertFrancis Can you remove unused Student fields from the UserInfoResult model and the code that builds it? Looks like it should just be PriorName, BannerId, Status, PrimaryMajor, AllMajors, RegistrationStatus, ClassLevel and ClassOf. I don't want other developers (or AI) to see these fields and infer they are only gated by the directory permissions, when many of them are more sensitive.

Comment thread web/Areas/RAPS/Services/UinformService.cs Fixed
Comment thread web/Areas/Directory/Services/UserInfoService.cs Fixed
Comment thread web/Areas/Directory/Services/UserInfoService.cs Fixed
Comment thread web/Areas/Directory/Services/UserInfoService.cs Fixed
Comment thread web/Areas/Directory/Services/UserInfoService.cs Fixed
Comment thread web/Areas/RAPS/Services/UinformService.cs Fixed
@rlorenzo
rlorenzo requested a balanced review from Copilot August 20, 2026 16:01
@JasonRobertFrancis

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review skipped: 221 files exceed the limit of 100.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 219 out of 221 changed files in this pull request and generated no new comments.

Suppressed comments (1)

web/Areas/Directory/Models/IndividualSearchResultWithIDs.cs:49

  • This branch removes the null-guard on PostalAddress, but LdapUserContact.PostalAddress is declared = null! and is only assigned when the LDAP entry actually contains a postalAddress attribute, so it can be null at runtime. For a contact without a postal address this will throw a NullReferenceException. Note the base class IndividualSearchResult handles the same field null-safely (ldapUserContact.PostalAddress?.Replace(...) ?? ""), so the two paths are now inconsistent. Please restore the null-safe access here.
                PostalAddress = ldapUserContact.PostalAddress.Replace("$", '\n'.ToString());

@rlorenzo rlorenzo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fresh pass over the branch at c18d9c8. Three authorization and student-data issues, two logic bugs in the Instinct lookup.

namespace Viper.Areas.Directory.Controllers
{
[Area("Directory")]
[Permission(Allow = "SVMSecure")]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@JasonRobertFrancis Line 12 allows all of SVMSecure while Table.cshtml:52 gates the link on SVMSecure.userinfo, so /userinfo/{mothraID} is reachable by anyone. Please match the controller to SVMSecure.userinfo and add the role lock DirectoryController:19 has.

if (individual != null) iamId = individual.IamId;

// Get user information
var userInfo = await _userInfo.GetUserInfoAsync(iamId, mothraID);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@JasonRobertFrancis GetUserInfoAsync runs at line 64, before the CanView* flags at 71-81, and UserInfoService.cs:113-126 populates all 11 sources unconditionally. Permissions need to gate the fetch, not just the rendering.

</div>
}
@* Student Information *@
@if (Model.IsStudent && Model.CanViewDirectoryDetail)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@JasonRobertFrancis StudentConfidentialScope is gone entirely rather than used, so the student block at line 144 renders prior name, Banner ID and majors for FERPA-blocked students. Please restore it as the gate.

if (!string.IsNullOrEmpty(tokenResponse.AccessToken))
{
// Cache token for slightly less than expiry time (subtract 2 hours as in CF code)
var cacheExpiry = TimeSpan.FromSeconds(tokenResponse.ExpiresIn - 7200); // 2 hours buffer

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@JasonRobertFrancis Line 1440 goes negative whenever the token lives under two hours, and ExpiresIn is 0 when expires_in is absent. IMemoryCache.Set then throws past the filter at 1455 and 500s the page, so clamp with Math.Max.

var apiUrl = _configuration["Instinct:ApiUrl"] ?? "https://uc-davis.api.instinctvet.com/";
var httpClient = _httpClientFactory.CreateClient();

var variablesJson = JsonSerializer.Serialize(new { name = lastName });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@JasonRobertFrancis Line 1326 searches last name only and 1340 takes FirstOrDefault on first name, so same-name people get each other's Instinct account and roles. Disambiguate on username, or treat multiple matches as none.

@rlorenzo

Copy link
Copy Markdown
Contributor

@JasonRobertFrancis Three threads still open after c18d9c8:

.gitignore:520-521 - .github/skills/impeccable/ and .github/hooks/impeccable.json still ignored.

Viper.csproj:9 - NU1902 still suppressed, which silences NuGet vulnerability advisories project-wide. NU1608 too.

UserInfoService.cs perf - reports-to N+1 is fixed. Still open: PopulateKeysAsync:1053 queries AAUD per key in the loop, GetUserPermissionsForSystemAsync runs 5x returning identical rows because systemPrefix never reaches the SQL, and .AsNoTracking() is absent throughout.

@rlorenzo rlorenzo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Medium and low items from the same pass, inline.

}";

// Execute GraphQL query
var apiUrl = _configuration["Instinct:ApiUrl"] ?? "https://uc-davis.api.instinctvet.com/";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@JasonRobertFrancis Production URL as the fallback means a misconfigured dev or test box hits prod Instinct. Same at 1398. Better to fail fast when the setting is missing.

{
if (HttpHelper.Settings != null)
{
optionsBuilder.UseSqlServer(HttpHelper.Settings["ConnectionStrings:Keys"]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@JasonRobertFrancis Unguarded UseSqlServer here overrides the DI registration, so Program.cs:226-229's VIPER string and UseCompatibilityLevel(130) are discarded for ConnectionStrings:Keys. Same in the other three new contexts, and no other context in the repo has an OnConfiguring.

return Ok(SessionTimeoutService.GetSessionTimeout(_viperContext));
}

[Route("/GetSessionTimeout")]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@JasonRobertFrancis Collides with #304, which adds a dedicated SessionTimeoutController for this and also edits SessionTimeoutService. Unrelated to userinfo, worth dropping from this PR.

Comment thread test/Usings.cs
@@ -1 +1,3 @@
global using Xunit;

[assembly: CollectionBehavior(DisableTestParallelization = true)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@JasonRobertFrancis Assembly level, so this disables parallelism for the whole suite, not just the new tests.

catch (Exception ex) when (ex is DbException || ex is InvalidOperationException)
{
// Exceptions during student info retrieval are caught and ignored to allow other directory details to load.
_logger.LogWarning(ex, "PopulateStudentInfoAsync failed");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@JasonRobertFrancis 27 catch blocks in this file and none rethrow, so a SIS or UCPath outage renders a page that looks complete with sections silently missing. Worth surfacing a partial-data notice.

public bool CanViewLoans { get; set; }
public bool CanViewInstinct { get; set; }
public bool CanViewADGroups { get; set; }
public bool IsOwnPage { get; set; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@JasonRobertFrancis Never assigned, but UserInfo.cshtml:12 reads it, so it is always false. Harmless only because UserInfoController:71 folds ownPage into CanViewDirectoryDetail.

return dt;
}

return null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@JasonRobertFrancis Returns null for anything it cannot parse, so a bad date disappears rather than erroring. Registered globally for DateTime?, so it affects every IAM response.

Comment thread scripts/lint-any.js
stdio: "pipe",
cwd: projectRoot,
encoding: "utf8",
shell: true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@JasonRobertFrancis shell: true routes these paths through cmd.exe on Windows, so a path with a space or & breaks. npx.cmd avoids it. Same at 273.

@rlorenzo

Copy link
Copy Markdown
Contributor

@JasonRobertFrancis I was testing this on TEST: https://secure-test.vetmed.ucdavis.edu/2/UserInfo/02725606 and comparing against VIPER1: https://secure-test.vetmed.ucdavis.edu/default.cfm?page=userinfo&id=1000610632&mothraID=02725606

  1. Why do 2 photos render?
  2. The UC Path section is missing "Position: 000652 APPLICATIONS PROGR 4 — 072000 VM: DEANS OFFICE" that is on VIPER1. Also, this must be a data issue, but why is my hire date set to 2/28/2025?
  3. For "System Permissions," VIPER has me with RAPS (52) and SVMSecure (356), but the VIPER2 version has me with RAPS (21), SVMSecure (291), and VIPERForms (1). Why the difference in numbers?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants