Skip to content

DT-1662: Ensure single Library Card per User #2536

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

Merged
merged 28 commits into from
May 28, 2025

Conversation

rushtong
Copy link
Contributor

@rushtong rushtong commented May 22, 2025

Addresses

https://broadworkbench.atlassian.net/browse/DT-1662

Summary

This PR enforces that a user can only have a single library card. There are a significant number of test changes to support this requirement. For backwards compatibility with the UI, we're populating the User object with a libraryCard and a list of libraryCards. When the UI is updated to match this, we can come back and remove the deprecated field.

Notes

There will need to be a follow-on PR in the UI to deal with the user model describing an array of library cards. See https://github.com/DataBiosphere/duos-ui/blob/ee9e70640759c33bed9e6b9f8b5393f4692dea8b/src/types/model.ts#L51 for where we need to start there.

See these tickets for that work:

There are deletes for duplicate user_id and user_email rows. In Prod, there are no records affected. In Dev and Staging, there are 2 and 1 record respectively that will be removed.

Testing

Tested the db update with a recent db download from

  • ✅ Dev
  • ✅ Staging
  • ✅ Prod

Tested the UI Admin/SO Library Card table functionality

  • ✅ Dev
  • ✅ Staging
  • ✅ Prod

Have you read CONTRIBUTING.md lately? If not, do that first.

  • Label PR with a Jira ticket number and include a link to the ticket
  • Label PR with a security risk modifier [no, low, medium, high]
  • PR describes scope of changes
  • Get a minimum of one thumbs worth of review, preferably two if enough team members are available
  • Get PO sign-off for all non-trivial UI or workflow changes
  • Verify all tests go green
  • Test this change deployed correctly and works on dev environment after deployment

+ " LEFT JOIN roles r ON r.role_id = ur.role_id "
+ " WHERE LOWER(u.email) = LOWER(:email) "
+ " AND r.role_id = :roleId")
User findUserByEmailAndRoleId(@Bind("email") String email, @Bind("roleId") Integer roleId);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Only used in tests

Comment on lines +77 to +78
// Some queries look for `user_role_id` while those that use a prefix look for `u_user_role_id`
private UserRole mapUserRoleFromRowView(RowView rowView, Integer userId) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In theory, I don't think this method should be necessary. However, I found that the mapping wasn't working for cases where the prefix was ur_ and couldn't get that to work like I think it should.

Comment on lines -189 to -190
User user = userService.findUserByEmail(authUser.getEmail());
List<LibraryCard> libraryCards = libraryCardService.findLibraryCardsByUserId(userId);
Copy link
Contributor Author

@rushtong rushtong May 23, 2025

Choose a reason for hiding this comment

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

This is a bug. When we remove DAAs from a library card, we need to remove them from the user passed into the method via userId. This code will remove a DAA from the AuthUser's library card.

Copy link
Contributor

Choose a reason for hiding this comment

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

Good catch!

Copy link
Contributor

Choose a reason for hiding this comment

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

Should this method also be checking that the SO is from the same institution as the user?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is currently not enabled in any environment so I want to tackle any explicit functional changes to DAA work as a distinct task. We will need to re-evaluate the entire feature before we do any further work here.

Copy link
Contributor

Choose a reason for hiding this comment

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

Made a ticket to remind ourselves: https://broadworkbench.atlassian.net/browse/DT-1704

@rushtong rushtong marked this pull request as ready for review May 23, 2025 13:43
@rushtong rushtong requested a review from a team as a code owner May 23, 2025 13:43
@rushtong rushtong requested review from fboulnois and rjohanek and removed request for a team May 23, 2025 13:43
@rushtong rushtong removed request for fboulnois and rjohanek May 23, 2025 15:49
Copilot

This comment was marked as resolved.

@rushtong rushtong requested a review from Copilot May 23, 2025 19:13
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR enforces that a user may have only a single library card by replacing all references to collections of library cards with a single library card field. Key changes include updating test cases to use setLibraryCard instead of setLibraryCards, modifying service methods in UserService and DataAccessRequestService to check for a single library card, and adjusting the DAO/mappers and associated YAML schema for consistency.

Reviewed Changes

Copilot reviewed 21 out of 23 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/test/java/org/broadinstitute/consent/http/service/DataAccessRequestServiceTest.java Updated tests to replace empty libraryCards checks with a null check on libraryCard
src/test/java/org/broadinstitute/consent/http/resources/DataAccessRequestResourceTest.java Modified tests to use setLibraryCard in place of setLibraryCards
src/main/resources/assets/schemas/User.yaml Deprecated the collections field and added the libraryCard field
src/main/java/org/broadinstitute/consent/http/service/UserService.java Adjusted user creation and retrieval to assign and output a single library card for backwards compatibility
(Other files) Consistent update from list-based library cards to a single library card across DAOs, mappers, and resource classes
Files not reviewed (2)
  • src/main/resources/changelog-master.xml: Language not supported
  • src/main/resources/changesets/changelog-consent-2025-23-unique-lc-user.xml: Language not supported

Copy link

Copy link
Contributor

@fboulnois fboulnois left a comment

Choose a reason for hiding this comment

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

Looks good, a suggestion:

ld.daa_id as lc_daa_id
FROM users u
LEFT JOIN user_role ur ON ur.user_id = u.user_id
LEFT JOIN roles r ON r.role_id = ur.role_id
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we pull this common section into something separate referenced by this method and the other one?

Copy link
Contributor

Choose a reason for hiding this comment

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

@fboulnois if we do that, won't we get complaints from SemGrep? Or are you suggesting we also change that configuration at this time?

Copy link
Contributor

Choose a reason for hiding this comment

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

If I had to guess, it's why this class is being refactored in this way.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If I had to guess, it's why this class is being refactored in this way.

This is the primary reason why this is refactored, yes. The string concatenation approach has been problematic to maintain and triggers semgrep issues. I would like to see if we can come up with a better way of re-using large query blocks in some other way, but for now, this is the least bad option as I see it.

@@ -191,7 +207,7 @@ void testFindUsersWithLCsAndInstitution() {

User user2 = createUserWithInstitution();
int lcId2 = libraryCardDAO.insertLibraryCard(user2.getUserId(),
user.getDisplayName(), user.getEmail(), user.getUserId(), new Date());
user2.getDisplayName(), user2.getEmail(), user2.getUserId(), new Date());
Copy link
Contributor

Choose a reason for hiding this comment

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

Good catch.

@@ -56,34 +48,56 @@ public void accumulate(Map<Integer, User> map, RowView rowView) {
}
}
} catch (MappingException e) {
//Ignore institution mapping errors, possible for new users to not have an institution
logDebug("Error adding Institution to User: %s".formatted(e.getMessage()));
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for adding the case of the debug logs. I find them helpful for local development.

Copy link
Contributor

Choose a reason for hiding this comment

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

@rushtong - Do I understand correctly that everyone can read every DAA?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@otchet-broad - yes, all DAAs are intentionally public.

((PSQLException) e.getCause()).getSQLState());
}
User user2 = createUser();
// Test Unique on library_card.user_email - note that we're using the same email as user1
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice!

@@ -101,7 +101,7 @@ public User(String json) {
// Nor do we need to retrieve the full institution object from user-provided data.
JsonObject filteredUserJsonObject = filterFields(
userJsonObject,
Arrays.asList("createDate", "institution", "libraryCards"));
Arrays.asList("createDate", "institution", "libraryCard"));
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we also have to support libraryCards until they're completely deprecated?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think we do. This method constructs a User object. Since User doesn't have a libraryCards field so if a user-provided string does contain libraryCards, it will be ignored on the next line in gson.fromJson()

@rushtong rushtong merged commit 37ff736 into develop May 28, 2025
16 checks passed
@rushtong rushtong deleted the gr-DT-1662-single-lc-per-user branch May 28, 2025 13:45
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.

3 participants