From d1b28af94144c7b4ceb92a017418163e4499d72d Mon Sep 17 00:00:00 2001 From: Your Name <5115845+dauglyon@users.noreply.github.com> Date: Fri, 29 May 2026 15:32:06 -0700 Subject: [PATCH 1/6] Validate signup username against backend format rules The frontend availability check compared availablename to username.toLowerCase(), so a username like "John" passed validation but was then rejected by auth2 for the uppercase letter. Inputs with characters auth2 strips (dots, hyphens, etc.) failed with a misleading "Username is not available" message. Mirror the kbase/auth2 NewUserName rules on the form: must start with a lowercase letter, only [a-z0-9_], no repeating or trailing underscores, at most 100 chars. Show a specific error for format violations and only treat availability mismatches as collisions. --- .../signup/AccountInformation.test.tsx | 68 +++++++++++++++++++ src/features/signup/AccountInformation.tsx | 41 +++++++++-- 2 files changed, 105 insertions(+), 4 deletions(-) diff --git a/src/features/signup/AccountInformation.test.tsx b/src/features/signup/AccountInformation.test.tsx index d491669b..82911794 100644 --- a/src/features/signup/AccountInformation.test.tsx +++ b/src/features/signup/AccountInformation.test.tsx @@ -135,4 +135,72 @@ describe('AccountInformation', () => { expect(mockNavigate).toHaveBeenCalledWith('/signup/3'); }); + + test.each([ + ['uppercase letters', 'BadUser'], + ['a leading digit', '1baduser'], + ['a hyphen', 'bad-user'], + ['a period', 'bad.user'], + ['repeating underscores', 'bad__user'], + ['a trailing underscore', 'baduser_'], + ])( + 'blocks submission and shows format error for username with %s', + async (_label, badName) => { + const store = createTestStore(); + store.dispatch( + setLoginData({ + creationallowed: true, + expires: 0, + login: [], + provider: 'Google', + create: [ + { + provemail: 'test@test.com', + provfullname: 'Test User', + availablename: 'testuser', + id: '123', + provusername: 'testuser', + }, + ], + }) + ); + renderWithProviders(, { store }); + + await act(() => { + fireEvent.change(screen.getByRole('textbox', { name: /Full Name/i }), { + target: { value: 'Test User' }, + }); + }); + await act(() => { + fireEvent.change(screen.getByRole('textbox', { name: /Email/i }), { + target: { value: 'test@test.com' }, + }); + }); + await act(() => { + fireEvent.change( + screen.getByRole('textbox', { name: /KBase Username/i }), + { target: { value: badName } } + ); + }); + await act(() => { + fireEvent.change( + screen.getByRole('textbox', { name: /Organization/i }), + { target: { value: 'Test Org' } } + ); + }); + await act(() => { + fireEvent.change(screen.getByRole('textbox', { name: /Department/i }), { + target: { value: 'Test Dept' }, + }); + }); + await act(() => { + fireEvent.submit(screen.getByTestId('accountinfoform')); + }); + + expect( + screen.getByText(/may contain only lowercase letters/i) + ).toBeInTheDocument(); + expect(mockNavigate).not.toHaveBeenCalledWith('/signup/3'); + } + ); }); diff --git a/src/features/signup/AccountInformation.tsx b/src/features/signup/AccountInformation.tsx index 156bd49a..6aeeefaf 100644 --- a/src/features/signup/AccountInformation.tsx +++ b/src/features/signup/AccountInformation.tsx @@ -58,8 +58,14 @@ export const AccountInformation: FC<{}> = () => { const [username, setUsername] = useState(account.username ?? ''); const userAvail = loginUsernameSuggest.useQuery(username); const nameShort = username.length < 3; - const nameAvail = - userAvail.currentData?.availablename === username.toLowerCase(); + const nameTooLong = username.length > 100; + // Mirrors backend rules in kbase/auth2 NewUserName: must start with a + // lowercase letter; only [a-z0-9_]; no repeating or trailing underscores. + const nameFormatValid = + /^[a-z][a-z0-9_]*$/.test(username) && + !username.includes('__') && + !username.endsWith('_'); + const nameAvail = userAvail.currentData?.availablename === username; const surveyQuestion = 'How did you hear about us? (select all that apply)'; const [optionalText, setOptionalText] = useState>({}); @@ -199,7 +205,11 @@ export const AccountInformation: FC<{}> = () => { required: true, onChange: (e) => setUsername(e.currentTarget.value), validate: () => - !nameShort && !userAvail.isFetching && nameAvail, + !nameShort && + !nameTooLong && + nameFormatValid && + !userAvail.isFetching && + nameAvail, })} defaultValue={account.username} helperText={ @@ -209,6 +219,24 @@ export const AccountInformation: FC<{}> = () => { Username is too short.
+ ) : nameTooLong ? ( + + Username must be at most 100 characters. +
+
+ ) : !nameFormatValid ? ( + + Username may contain only lowercase letters, digits, and + underscores, and must start with a letter. Underscores + cannot repeat or end the username. + {userAvail.currentData?.availablename ? ( + <> + {' '} + Suggested: "{userAvail.currentData.availablename}". + + ) : null} +
+
) : !nameAvail && !userAvail.isFetching ? ( Username is not available. Suggested: " @@ -224,7 +252,12 @@ export const AccountInformation: FC<{}> = () => { } - error={nameShort || (!userAvail.isFetching && !nameAvail)} + error={ + nameShort || + nameTooLong || + !nameFormatValid || + (!userAvail.isFetching && !nameAvail) + } /> From ec1804fd53fb3d7e99033dfd1bf1337811c28ece Mon Sep 17 00:00:00 2001 From: Your Name <5115845+dauglyon@users.noreply.github.com> Date: Fri, 29 May 2026 15:32:20 -0700 Subject: [PATCH 2/6] Gate CDM sidebar link on BERDL_USER role The CDM nav item was gated on CDM_JUPYTERHUB_ADMIN, so only admins saw the link. Per the BERDL platform docs, BERDL_USER is the role that gates access to the lakehouse; CDM_JUPYTERHUB_ADMIN is a separate admin role for approving access requests. --- src/features/layout/LeftNavBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/layout/LeftNavBar.tsx b/src/features/layout/LeftNavBar.tsx index 32817209..e6274be1 100644 --- a/src/features/layout/LeftNavBar.tsx +++ b/src/features/layout/LeftNavBar.tsx @@ -72,7 +72,7 @@ const LeftNavBar: FC = () => { icon={faDatabase} badge={'alpha'} badgeColor={'warning'} - requiredRole="CDM_JUPYTERHUB_ADMIN" + requiredRole="BERDL_USER" />
    From 349922d74a8328b117e28a9b87cb2dbeb4882243 Mon Sep 17 00:00:00 2001 From: Your Name <5115845+dauglyon@users.noreply.github.com> Date: Mon, 1 Jun 2026 09:17:07 -0700 Subject: [PATCH 3/6] Skip user username pre-fill on ORCID signup ORCID's provider-supplied username is the numeric ORCID iD (e.g. 0000-0002-1825-0097). auth2's NewUserName.sanitizeName strips all of that to empty and getAvailableUserName falls back to user, so every ORCID signup landed on the form with user1 (or user2, ...) already in the username field. Leave the username blank when the provider is OrcID so the user picks their own. Display name and email pre-fill are unchanged. --- src/features/signup/SignupSlice.test.tsx | 43 ++++++++++++++++++++++++ src/features/signup/SignupSlice.tsx | 13 +++++-- 2 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 src/features/signup/SignupSlice.test.tsx diff --git a/src/features/signup/SignupSlice.test.tsx b/src/features/signup/SignupSlice.test.tsx new file mode 100644 index 00000000..9762bf9d --- /dev/null +++ b/src/features/signup/SignupSlice.test.tsx @@ -0,0 +1,43 @@ +import { createTestStore } from '../../app/store'; +import { setLoginData } from './SignupSlice'; + +const makeLoginData = ( + provider: string, + availablename: string +): Parameters[0] => ({ + creationallowed: true, + expires: 0, + login: [], + provider, + create: [ + { + provemail: 'jane@example.com', + provfullname: 'Jane Doe', + availablename, + id: '123', + provusername: '0000-0002-1825-0097', + }, + ], +}); + +describe('signup setLoginData', () => { + test('pre-fills username from availablename for non-ORCID providers', () => { + const store = createTestStore(); + store.dispatch(setLoginData(makeLoginData('Google', 'janedoe'))); + expect(store.getState().signup.account.username).toBe('janedoe'); + }); + + test('leaves username blank for ORCID logins to avoid user default', () => { + const store = createTestStore(); + store.dispatch(setLoginData(makeLoginData('OrcID', 'user1'))); + expect(store.getState().signup.account.username).toBeUndefined(); + }); + + test('still pre-fills display name and email for ORCID', () => { + const store = createTestStore(); + store.dispatch(setLoginData(makeLoginData('OrcID', 'user1'))); + const account = store.getState().signup.account; + expect(account.display).toBe('Jane Doe'); + expect(account.email).toBe('jane@example.com'); + }); +}); diff --git a/src/features/signup/SignupSlice.tsx b/src/features/signup/SignupSlice.tsx index 7c398b45..ec5d6fbd 100644 --- a/src/features/signup/SignupSlice.tsx +++ b/src/features/signup/SignupSlice.tsx @@ -44,9 +44,16 @@ export const signupSlice = createSlice({ // Set provider creeation data state.loginData = action.payload; // Set account defaults from provider - state.account.display = action.payload?.create[0].provfullname; - state.account.email = action.payload?.create[0].provemail; - state.account.username = action.payload?.create[0].availablename; + const detail = action.payload?.create[0]; + state.account.display = detail?.provfullname; + state.account.email = detail?.provemail; + // ORCID's provusername is the numeric ORCID iD, which auth2 cannot + // sanitize into a valid username and falls back to user. Leave the + // field blank for ORCID so the user picks their own. + state.account.username = + action.payload?.provider === 'OrcID' + ? undefined + : detail?.availablename; }, setAccount: ( state, From 171749b5ef8bab50edd40d2fbf4d0d1ce3e21808 Mon Sep 17 00:00:00 2001 From: Your Name <5115845+dauglyon@users.noreply.github.com> Date: Thu, 10 Sep 2026 09:54:37 -0700 Subject: [PATCH 4/6] Rename the CDM sidebar link and redirect to BERDL The link goes to hub.berdl.kbase.us, the BERDL JupyterHub. Rename the nav label, route, component, deploy config key, and build env var to match. The CI hub's login page (kbase/cdm-jupyterhub) still links to /cdm/redirect, so that path redirects to the new route. --- config.json | 4 ++-- scripts/build_deploy.ts | 6 +++--- src/app/Routes.tsx | 17 +++++++++++++---- .../CDMRedirect.tsx => berdl/BERDLRedirect.tsx} | 6 +++--- src/features/layout/LeftNavBar.tsx | 4 ++-- 5 files changed, 23 insertions(+), 14 deletions(-) rename src/features/{cdm/CDMRedirect.tsx => berdl/BERDLRedirect.tsx} (82%) diff --git a/config.json b/config.json index 70b54adb..eae5f2c6 100644 --- a/config.json +++ b/config.json @@ -10,7 +10,7 @@ "domain": "ci.kbase.us", "legacy": "legacy.ci.kbase.us", "public_url": "/", - "cdm_domain": "cdmhub.ci.kbase.us" + "berdl_domain": "cdmhub.ci.kbase.us" }, "ci-europa": { @@ -47,7 +47,7 @@ "name": "kbase_session_backup", "domain": ".kbase.us" }, - "cdm_domain": "hub.berdl.kbase.us", + "berdl_domain": "hub.berdl.kbase.us", "redirect_whitelist": ["*.berdl.kbase.us"] } } diff --git a/scripts/build_deploy.ts b/scripts/build_deploy.ts index 623e17ae..65d1bd5a 100755 --- a/scripts/build_deploy.ts +++ b/scripts/build_deploy.ts @@ -18,7 +18,7 @@ interface EnvironmentConfig { name: string; domain: string; }; - cdm_domain?: string; + berdl_domain?: string; redirect_whitelist?: string[]; } @@ -54,7 +54,7 @@ const setEnvironment = ( legacy, public_url: publicURL, backup_cookie: backupCookie, - cdm_domain: cdmDomain, + berdl_domain: berdlDomain, redirect_whitelist: redirectWhitelist, } = environmentConfig; @@ -66,7 +66,7 @@ const setEnvironment = ( REACT_APP_KBASE_LEGACY_DOMAIN: legacy, REACT_APP_KBASE_BACKUP_COOKIE_NAME: backupCookie?.name || '', REACT_APP_KBASE_BACKUP_COOKIE_DOMAIN: backupCookie?.domain || '', - REACT_APP_KBASE_CDM_DOMAIN: cdmDomain || 'cdmhub.' + domain, + REACT_APP_KBASE_BERDL_DOMAIN: berdlDomain || 'cdmhub.' + domain, REACT_APP_REDIRECT_WHITELIST: redirectWhitelist?.join(',') || '', }; Object.assign(process.env, envsNew); diff --git a/src/app/Routes.tsx b/src/app/Routes.tsx index ccbfd694..a959c53f 100644 --- a/src/app/Routes.tsx +++ b/src/app/Routes.tsx @@ -38,7 +38,7 @@ import { LogInSessions } from '../features/account/LogInSessions'; import { UseAgreements } from '../features/account/UseAgreements'; import { skipToken } from '@reduxjs/toolkit/dist/query'; import { getMe } from '../common/api/authService'; -import { CDMRedirect } from '../features/cdm/CDMRedirect'; +import { BERDLRedirect } from '../features/berdl/BERDLRedirect'; import { OrcidLink, OrcidLinkContinue, @@ -147,10 +147,19 @@ const Routes: FC = () => { } /> - {/* CDM */} - - } />} /> + {/* BERDL */} + + } />} + /> + {/* The CI hub's login page (kbase/cdm-jupyterhub templates/login.html) + still sends users to the pre-rename route. */} + } + /> {/* IFrame Fallback Routes */} diff --git a/src/features/cdm/CDMRedirect.tsx b/src/features/berdl/BERDLRedirect.tsx similarity index 82% rename from src/features/cdm/CDMRedirect.tsx rename to src/features/berdl/BERDLRedirect.tsx index 656c47e7..f017a3f3 100644 --- a/src/features/cdm/CDMRedirect.tsx +++ b/src/features/berdl/BERDLRedirect.tsx @@ -2,9 +2,9 @@ import { Container, Stack } from '@mui/system'; import { useEffect } from 'react'; import { Loader } from '../../common/components'; -export const CDMRedirect = () => { +export const BERDLRedirect = () => { useEffect(() => { - window.location.href = `https://${process.env.REACT_APP_KBASE_CDM_DOMAIN}/hub`; + window.location.href = `https://${process.env.REACT_APP_KBASE_BERDL_DOMAIN}/hub`; }); return ( @@ -16,7 +16,7 @@ export const CDMRedirect = () => { justifyContent={'center'} > -
    Redirecting to CDM
    +
    Redirecting to BERDL
    ); diff --git a/src/features/layout/LeftNavBar.tsx b/src/features/layout/LeftNavBar.tsx index e6274be1..4ebb2cb0 100644 --- a/src/features/layout/LeftNavBar.tsx +++ b/src/features/layout/LeftNavBar.tsx @@ -67,8 +67,8 @@ const LeftNavBar: FC = () => { badgeColor={'primary'} /> Date: Thu, 10 Sep 2026 10:02:48 -0700 Subject: [PATCH 5/6] Log in to GHCR with the workflow token instead of the retired bot secret kbase/.github#70 (2026-04-10) removed the kbase-bot GHCR_TOKEN from the org and moved the shared build-push workflow to github.token. This repo's local copy of that workflow still used the secret, so every build since has failed at the login step before lint, tests, or the image build ran. --- .github/workflows/reusable_build-push.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/reusable_build-push.yml b/.github/workflows/reusable_build-push.yml index d68782cd..3ba43911 100644 --- a/.github/workflows/reusable_build-push.yml +++ b/.github/workflows/reusable_build-push.yml @@ -14,6 +14,9 @@ on: jobs: build-push-image: runs-on: ubuntu-latest + permissions: + contents: read + packages: write steps: - name: Check out GitHub Repo uses: actions/checkout@v3 @@ -26,12 +29,11 @@ jobs: uses: docker/setup-buildx-action@v2 - name: Login to GHCR - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io - # note that the calling workflow must set `secrets: inherit` - username: '${{ secrets.GHCR_USERNAME }}' - password: '${{ secrets.GHCR_TOKEN }}' + username: ${{ github.actor }} + password: ${{ github.token }} - name: Set up Node.js uses: actions/setup-node@v2 From 3eeee99d4c333c2a0709e77c594a4f0bba5758d0 Mon Sep 17 00:00:00 2001 From: Your Name <5115845+dauglyon@users.noreply.github.com> Date: Thu, 10 Sep 2026 10:12:08 -0700 Subject: [PATCH 6/6] Name the hub link Lakehouse rather than BERDL Lakehouse is the user-facing name for hub.berdl.kbase.us. Rename the nav label, route, component, deploy config key, and build env var to match; /cdm/redirect still redirects to the new route. --- config.json | 4 ++-- scripts/build_deploy.ts | 6 +++--- src/app/Routes.tsx | 10 +++++----- .../LakehouseRedirect.tsx} | 6 +++--- src/features/layout/LeftNavBar.tsx | 4 ++-- 5 files changed, 15 insertions(+), 15 deletions(-) rename src/features/{berdl/BERDLRedirect.tsx => lakehouse/LakehouseRedirect.tsx} (81%) diff --git a/config.json b/config.json index eae5f2c6..340d8365 100644 --- a/config.json +++ b/config.json @@ -10,7 +10,7 @@ "domain": "ci.kbase.us", "legacy": "legacy.ci.kbase.us", "public_url": "/", - "berdl_domain": "cdmhub.ci.kbase.us" + "lakehouse_domain": "cdmhub.ci.kbase.us" }, "ci-europa": { @@ -47,7 +47,7 @@ "name": "kbase_session_backup", "domain": ".kbase.us" }, - "berdl_domain": "hub.berdl.kbase.us", + "lakehouse_domain": "hub.berdl.kbase.us", "redirect_whitelist": ["*.berdl.kbase.us"] } } diff --git a/scripts/build_deploy.ts b/scripts/build_deploy.ts index 65d1bd5a..b7c56dca 100755 --- a/scripts/build_deploy.ts +++ b/scripts/build_deploy.ts @@ -18,7 +18,7 @@ interface EnvironmentConfig { name: string; domain: string; }; - berdl_domain?: string; + lakehouse_domain?: string; redirect_whitelist?: string[]; } @@ -54,7 +54,7 @@ const setEnvironment = ( legacy, public_url: publicURL, backup_cookie: backupCookie, - berdl_domain: berdlDomain, + lakehouse_domain: lakehouseDomain, redirect_whitelist: redirectWhitelist, } = environmentConfig; @@ -66,7 +66,7 @@ const setEnvironment = ( REACT_APP_KBASE_LEGACY_DOMAIN: legacy, REACT_APP_KBASE_BACKUP_COOKIE_NAME: backupCookie?.name || '', REACT_APP_KBASE_BACKUP_COOKIE_DOMAIN: backupCookie?.domain || '', - REACT_APP_KBASE_BERDL_DOMAIN: berdlDomain || 'cdmhub.' + domain, + REACT_APP_KBASE_LAKEHOUSE_DOMAIN: lakehouseDomain || 'cdmhub.' + domain, REACT_APP_REDIRECT_WHITELIST: redirectWhitelist?.join(',') || '', }; Object.assign(process.env, envsNew); diff --git a/src/app/Routes.tsx b/src/app/Routes.tsx index a959c53f..40fb77d3 100644 --- a/src/app/Routes.tsx +++ b/src/app/Routes.tsx @@ -38,7 +38,7 @@ import { LogInSessions } from '../features/account/LogInSessions'; import { UseAgreements } from '../features/account/UseAgreements'; import { skipToken } from '@reduxjs/toolkit/dist/query'; import { getMe } from '../common/api/authService'; -import { BERDLRedirect } from '../features/berdl/BERDLRedirect'; +import { LakehouseRedirect } from '../features/lakehouse/LakehouseRedirect'; import { OrcidLink, OrcidLinkContinue, @@ -147,18 +147,18 @@ const Routes: FC = () => { } /> - {/* BERDL */} - + {/* Lakehouse */} + } />} + element={} />} /> {/* The CI hub's login page (kbase/cdm-jupyterhub templates/login.html) still sends users to the pre-rename route. */} } + element={} /> {/* IFrame Fallback Routes */} diff --git a/src/features/berdl/BERDLRedirect.tsx b/src/features/lakehouse/LakehouseRedirect.tsx similarity index 81% rename from src/features/berdl/BERDLRedirect.tsx rename to src/features/lakehouse/LakehouseRedirect.tsx index f017a3f3..027670b4 100644 --- a/src/features/berdl/BERDLRedirect.tsx +++ b/src/features/lakehouse/LakehouseRedirect.tsx @@ -2,9 +2,9 @@ import { Container, Stack } from '@mui/system'; import { useEffect } from 'react'; import { Loader } from '../../common/components'; -export const BERDLRedirect = () => { +export const LakehouseRedirect = () => { useEffect(() => { - window.location.href = `https://${process.env.REACT_APP_KBASE_BERDL_DOMAIN}/hub`; + window.location.href = `https://${process.env.REACT_APP_KBASE_LAKEHOUSE_DOMAIN}/hub`; }); return ( @@ -16,7 +16,7 @@ export const BERDLRedirect = () => { justifyContent={'center'} > -
    Redirecting to BERDL
    +
    Redirecting to Lakehouse
    ); diff --git a/src/features/layout/LeftNavBar.tsx b/src/features/layout/LeftNavBar.tsx index 4ebb2cb0..f50c0923 100644 --- a/src/features/layout/LeftNavBar.tsx +++ b/src/features/layout/LeftNavBar.tsx @@ -67,8 +67,8 @@ const LeftNavBar: FC = () => { badgeColor={'primary'} />