[#13691] Migrate storage + logic layer from google id#13755
[#13691] Migrate storage + logic layer from google id#13755Nsohko wants to merge 5 commits intoTEAMMATES:feat/oidcfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR is part of issue #13691 and continues the migration away from using googleId as the stable account key by introducing OIDC concepts (issuer + subject) and shifting internal lookups toward Account UUIDs.
Changes:
- Introduces
LoginIssuer(+ CRUD db/logic) to map OIDC issuer URLs to provider names. - Refactors SQL storage/logic APIs and tests to use
accountId(UUID) instead ofgoogleIdfor linking and queries. - Replaces Google ID field validation/tests with OIDC issuer/subject validation.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| src/test/java/teammates/test/BaseTestCase.java | Updates typical Account construction to include issuer/subject/loginIdentifier. |
| src/test/java/teammates/storage/sqlapi/LoginIssuerDbTest.java | Adds unit tests for LoginIssuerDb CRUD behaviors. |
| src/test/java/teammates/storage/sqlapi/AccountsDbTest.java | Updates account tests to use UUID-based APIs; adds null-delete coverage. |
| src/test/java/teammates/sqllogic/core/UsersLogicTest.java | Updates reset flows to accountId and adds tests for attaching accounts to instructors. |
| src/test/java/teammates/sqllogic/core/DataBundleLogicTest.java | Updates data bundle deletion assertions to delete accounts by UUID. |
| src/test/java/teammates/sqllogic/core/AccountsLogicTest.java | Updates delete/join tests for UUID-based account APIs and new join flow. |
| src/test/java/teammates/common/util/FieldValidatorTest.java | Replaces Google ID validation tests with OIDC issuer/subject tests. |
| src/main/java/teammates/storage/sqlentity/User.java | Replaces getGoogleId helper with getAccountId helper. |
| src/main/java/teammates/storage/sqlentity/LoginIssuer.java | Adds new SQL entity for issuer→provider mapping. |
| src/main/java/teammates/storage/sqlentity/Account.java | Refactors Account to store LoginIssuer, OIDC subject, and a loginIdentifier; adds uniqueness constraint. |
| src/main/java/teammates/storage/sqlapi/UsersDb.java | Refactors user lookup helpers from googleId to accountId; updates search to use loginIdentifier. |
| src/main/java/teammates/storage/sqlapi/LoginIssuerDb.java | Adds persistence layer for LoginIssuer. |
| src/main/java/teammates/storage/sqlapi/AccountsDb.java | Removes googleId natural-id lookup; updates create logic to UUID-based access. |
| src/main/java/teammates/sqllogic/core/UsersLogic.java | Refactors joins/resets and helper methods to accountId; adds attach-account helper. |
| src/main/java/teammates/sqllogic/core/LoginIssuerLogic.java | Adds logic wrapper for LoginIssuerDb. |
| src/main/java/teammates/sqllogic/core/LogicStarter.java | Wires LoginIssuerLogic into dependency initialization. |
| src/main/java/teammates/sqllogic/core/DataBundleLogic.java | Updates account deletion in bundle removal to UUID-based deletion. |
| src/main/java/teammates/sqllogic/core/CoursesLogic.java | Updates course creation and student course listing to accountId-based APIs. |
| src/main/java/teammates/sqllogic/core/AccountsLogic.java | Refactors delete/join flows to accountId and delegates instructor linking to UsersLogic. |
| src/main/java/teammates/sqllogic/api/Logic.java | Updates public SQL logic API surface to accountId + adds LoginIssuer APIs. |
| src/main/java/teammates/common/util/SanitizationHelper.java | Replaces googleId sanitizer with loginIdentifier sanitizer. |
| src/main/java/teammates/common/util/HibernateUtil.java | Registers LoginIssuer as a managed Hibernate entity. |
| src/main/java/teammates/common/util/FieldValidator.java | Adds OIDC issuer/subject validation; removes Google ID validation. |
| src/main/java/teammates/common/util/Const.java | Adds OIDC-related constants used by validation. |
| src/main/java/teammates/common/datatransfer/OidcProviderNameType.java | Adds provider name enum for LoginIssuer. |
Comments suppressed due to low confidence (1)
src/main/java/teammates/storage/sqlentity/Account.java:159
- Account#getInvalidityInfo does not validate required linkage fields like loginIssuer (non-null) or loginIdentifier (non-null), so Account.isValid() may return true while persistence fails due to NOT NULL / FK constraints. Consider adding explicit validation for loginIssuer (non-null and/or loginIssuer.isValid()) and loginIdentifier (non-null/length/format as appropriate) to keep errors consistent with other entities.
@Override
public List<String> getInvalidityInfo() {
List<String> errors = new ArrayList<>();
addNonEmptyError(FieldValidator.getInvalidityInfoForOidcSubject(subject), errors);
addNonEmptyError(FieldValidator.getInvalidityInfoForPersonName(name), errors);
addNonEmptyError(FieldValidator.getInvalidityInfoForEmail(email), errors);
return errors;
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Issuer scheme is always HTTPS as per the spec. So what's the use of this constant
|
|
||
| return null; | ||
| } | ||
| } No newline at end of file |
There was a problem hiding this comment.
Please add newline at EOF
| && !issuerUri.isOpaque(); | ||
|
|
||
| if (!isValid) { | ||
| return getPopulatedErrorMessage(OIDC_ISSUER_ERROR_MESSAGE, issuer, OIDC_ISSUER_FIELD_NAME, |
There was a problem hiding this comment.
Can this be simplified to reduce duplication?
| * contains scheme, host, and optionally, port number and path components and no query or fragment components. | ||
| * | ||
| * @return An explanation of why the {@code issuer} is not acceptable. | ||
| * Returns an empty string if the {@code issuer} is acceptable. |
There was a problem hiding this comment.
Wrong comment? "returns.."
| /** | ||
| * Updates an issuer to provider name mapping. | ||
| */ | ||
| public LoginIssuer updateLoginIssuer(LoginIssuer loginIssuer) |
There was a problem hiding this comment.
When would we need this? Seems unlikely that we need to update a mapping
| * privileges. | ||
| * | ||
| * @param instructorGoogleId the Google ID of the instructor creating the | ||
| * @param instructorAccountId the Account ID of the instructor creating the |
There was a problem hiding this comment.
I think account should not be capitalised
| * Parameters regkey and googleId are non-null. | ||
| * Validates that an existing instructor can be linked to an existing account, then performs the link. | ||
| * | ||
| * <p>This is a lower-level helper flow than the registration-key-based join path.</p> |
There was a problem hiding this comment.
Why do we need this comment?
| if (googleId == null) { | ||
| throw new InvalidParametersException("Instructor's googleId cannot be null"); | ||
| if (accountId == null) { | ||
| throw new InvalidParametersException("Instructor's account Id cannot be null"); |
| validateJoinCourseRequest(googleId, instructor); | ||
| return usersLogic.joinCourseForInstructor(googleId, instructor); | ||
| validateInstructorAccountLinkRequest(accountId, instructor); | ||
| return usersLogic.attachAccountToInstructor(accountId, instructor); |
There was a problem hiding this comment.
I personally think "link" sounds better
|
|
||
| // Update the googleId of the student entity for the instructor which was created from sample data. | ||
| // If this instructor already has a student record in the same course | ||
| // without an existing account (e.g. from sample/imported data), |
There was a problem hiding this comment.
This is not possible due to unique constrant in schema
| if (providerName == null) { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
Maybe can use StringHelper.isEmpty for null check
|
|
||
| public static final String GOOGLE_ID_FIELD_NAME = "Google ID"; | ||
| public static final int GOOGLE_ID_MAX_LENGTH = 254; | ||
| public static final String OIDC_ISSUER_FIELD_NAME = "OIDC issuer"; |
There was a problem hiding this comment.
Should there be a max length for issuer too?
| } | ||
| } else { | ||
| // Check if this Google ID has already joined this course | ||
| // Check if this Account ID has already joined this course |
There was a problem hiding this comment.
The A in Account should be lowercase to be uniform with UserInfo
| if (instructorForKey.isRegistered()) { | ||
| if (instructorForKey.getGoogleId().equals(googleId)) { | ||
| Account existingAccount = accountsDb.getAccountByGoogleId(googleId); | ||
| if (instructorForKey.getAccountId().equals(accountId)) { | ||
| Account existingAccount = accountsDb.getAccount(accountId); | ||
| if (existingAccount != null) { | ||
| throw new EntityAlreadyExistsException("Instructor has already joined course"); | ||
| } | ||
| } else { | ||
| throw new EntityAlreadyExistsException("Instructor has already joined course"); | ||
| } | ||
| } else { |
There was a problem hiding this comment.
Maybe can consider guard clause to avoid deep nesting here.
| * Returns a list of {@link Course} for all courses a given student is enrolled in. | ||
| * | ||
| * @param googleId The Google ID of the student | ||
| * @param accountId The Account ID of the student |
|
|
||
| if (getAccountByGoogleId(account.getGoogleId()) != null) { | ||
| if (getAccount(account.getId()) != null || | ||
| getAccountByLoginIssuerAndSub(account.getLoginIssuer(), account.getOidcSubject())!= null) { |
There was a problem hiding this comment.
Should have a space between operand and operator at account.getOidcSubject())!=
Part of #13691