From f7d2b36428bc80ae728d11e5315e2cf83d93b452 Mon Sep 17 00:00:00 2001 From: FOLIO Translations Bot <38661258+folio-translations@users.noreply.github.com> Date: Fri, 27 Dec 2024 18:36:39 -0500 Subject: [PATCH 1/4] Update translation strings This pull request was initiated by Lokalise (user Peter Murray) at 2024-12-28 00:07:33 --- translations/stripes-core/zh_TW.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/translations/stripes-core/zh_TW.json b/translations/stripes-core/zh_TW.json index c53dddb7..45dddb49 100644 --- a/translations/stripes-core/zh_TW.json +++ b/translations/stripes-core/zh_TW.json @@ -84,7 +84,7 @@ "settingPassword": "正在設定密碼...", "mainnav.showAllApplicationsButtonLabel": "應用程式", "mainnav.showAllApplicationsButtonAriaLabel": "顯示所有應用程式", - "mainnav.currentAppAriaLabel": "目前開啟的應用程式: {appName} (點擊返回)", + "mainnav.currentAppAriaLabel": "目前開啟的應用程式 : {appName} ( 點擊返回 )", "mainnav.topLevelLabel": "主要", "mainnav.applicationListLabel": "應用程式清單", "errors.default.error": "抱歉,輸入的資訊與我們的紀錄不符。", @@ -122,7 +122,7 @@ "errors.password.whiteSpace.invalid": "密碼不能含有空格。", "mainnav.myProfileAriaLabel": "{tenantName} {servicePointName}設定檔", "mainnav.skipMainNavigation": "跳過主導覽", - "errors.user.timeout": "連線已過期。請再次登入以恢復連線。", + "errors.user.timeout": "工作階段已過期。請再次登入以恢復工作階段。", "errors.password.consecutiveWhitespaces.invalid": "密碼不得包含連續的空格字元。", "about.missingModuleCount": "{count, number}缺少{count, plural, one {介面} other {介面}}} }}", "about.incompatibleModuleCount": "{count, number}不相容於介面{count, plural, one { version } other { versions }}", @@ -130,16 +130,16 @@ "routeErrorBoundary.goToAppHomeLabel": "返回首頁", "routeErrorBoundary.goToModuleHomeLabel": "返回{name}登入頁", "routeErrorBoundary.goToModuleSettingsHomeLabel": "返回{name}設定", - "logoutKeepSso": "從 FOLIO 登出,保持 SSO 連線", + "logoutKeepSso": "從 FOLIO 登出,保持 SSO 連線工作階段", "mainnav.profileDropdown.locale": "語言環境", "mainnav.profileDropdown.permissions": "權限", "front.error.header": "錯誤 404", "errors.password.compromised.invalid": "密碼不得為常用、被預測或被外洩的密碼", "createResetPassword.ruleTemplate": "必須{description}", "front.error.general.message": "在此伺服器上找不到請求的 URL {br}{url}{br} 。", - "front.error.setPassword.message": "請登出目前的 FOLIO 連線以設定密碼。登出後,請嘗試使用此連結再次設定您的密碼。", + "front.error.setPassword.message": "請登出目前的 FOLIO 工作階段以設定密碼。登出後,請嘗試使用此連結再次設定您的密碼。", "title.noPermission": "沒有權限", - "front.error.noPermission": "您無權查看此應用程式/記錄", + "front.error.noPermission": "您無權查看此應用程式 / 紀錄", "button.duplicate": "複製", "stale.warning": "伺服器上的應用程式已更改,需要重新整理。", "stale.reload": "點擊此處重新載入。", @@ -151,7 +151,7 @@ "rtr.idleSession.timeRemaining": "Time remaining", "rtr.idleSession.keepWorking": "Keep working", "rtr.idleSession.sessionExpiredSoSad": "Your session expired due to inactivity.", - "rtr.idleSession.logInAgain": "Log in again", + "rtr.idleSession.logInAgain": "再次登入", "title.logout": "Log out", "about.applicationCount": "{count} applications", "about.applicationsVersionsTitle": "Applications/modules/interfaces", From 2f75d430cc24a758073dd8dbe3f71d4d518de16b Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Thu, 9 Jan 2025 16:59:06 -0500 Subject: [PATCH 2/4] STCOR-933 fetch /saml/check when starting a new session (#1582) This is a follow-up to the botched implementation of [STCOR-816](https://folio-org.atlassian.net/browse/STCOR-816) in PR #1432. The description there is "When restoring an existing session..." but in fact the implementation is "When starting a new session or restoring an existing one...". When starting a new session, however, discovery has not happened yet, so there is no information about what interfaces are present, and the conditional would always return false. The implementation here corrects the conditional: 1. if we are starting a new session 2. if we are resuming an existing session and the `login-saml` interface is present. Refs STCOR-933 --- CHANGELOG.md | 1 + src/loginServices.js | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6032c8b1..984f777b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * Don't override initial discovery and okapi data in test mocks. Refs STCOR-913. * `` must consume `QueryClient` in order to supply it to `loginServices::logout()`. Refs STCOR-907. * On resuming session, spread session and `_self` together to preserve session values. Refs STCOR-912. +* Fetch `/saml/check` when starting a new session, i.e. before discovery. Refs STCOR-933, STCOR-816. ## [10.2.0](https://github.com/folio-org/stripes-core/tree/v10.2.0) (2024-10-11) [Full Changelog](https://github.com/folio-org/stripes-core/compare/v10.1.1...v10.2.0) diff --git a/src/loginServices.js b/src/loginServices.js index 5a38dc46..71670c70 100644 --- a/src/loginServices.js +++ b/src/loginServices.js @@ -851,8 +851,11 @@ export function checkOkapiSession(okapiUrl, store, tenant) { .then((sess) => { return sess?.user?.id ? validateUser(okapiUrl, store, tenant, sess) : null; }) - .then(() => { - if (store.getState().discovery?.interfaces?.['login-saml']) { + .then((res) => { + // check whether SSO is enabled if either + // 1. res is null (when we are starting a new session) + // 2. login-saml interface is present (when we are resuming an existing session) + if (!res || store.getState().discovery?.interfaces?.['login-saml']) { return getSSOEnabled(okapiUrl, store, tenant); } return Promise.resolve(); From 983ce869e2c6853ca683493b54763d8006df38e8 Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Thu, 9 Jan 2025 17:08:47 -0500 Subject: [PATCH 3/4] STCOR-926 validate that cookies and storage are available (#1581) Detect the presence of localStorage, sessionStorage, and cookies early early early in the stripes-init process and show an error message if any are unavailable. This prevents a white screen of death if, say, session storage is unavailable but we call it anyway, resulting in an untrapped exception (Here's looking at you, OIDCRedirect). Refs STCOR-926 --- src/App.js | 63 ++++++++++++++++--- src/App.test.js | 36 +++++++++++ .../AppConfigError/AppConfigError.css | 32 ++++++++++ .../AppConfigError/AppConfigError.js | 47 ++++++++++++++ .../AppConfigError/AppConfigError.test.js | 12 ++++ src/components/AppConfigError/index.js | 1 + src/components/OIDCRedirect.js | 10 ++- 7 files changed, 192 insertions(+), 9 deletions(-) create mode 100644 src/App.test.js create mode 100644 src/components/AppConfigError/AppConfigError.css create mode 100644 src/components/AppConfigError/AppConfigError.js create mode 100644 src/components/AppConfigError/AppConfigError.test.js create mode 100644 src/components/AppConfigError/index.js diff --git a/src/App.js b/src/App.js index 7f72ce32..4cd8ca07 100644 --- a/src/App.js +++ b/src/App.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { okapi as okapiConfig, config } from 'stripes-config'; import merge from 'lodash/merge'; +import AppConfigError from './components/AppConfigError'; import connectErrorEpic from './connectErrorEpic'; import configureEpics from './configureEpics'; import configureLogger from './configureLogger'; @@ -23,6 +24,39 @@ const StrictWrapper = ({ children }) => { return {children}; }; +/** + * isStorageEnabled + * Return true if local-storage, session-storage, and cookies are all enabled. + * Return false otherwise. + * @returns boolean true if storages are enabled; false otherwise. + */ +export const isStorageEnabled = () => { + let isEnabled = true; + // local storage + try { + localStorage.getItem('test-key'); + } catch (e) { + console.warn('local storage is disabled'); // eslint-disable-line no-console + isEnabled = false; + } + + // session storage + try { + sessionStorage.getItem('test-key'); + } catch (e) { + console.warn('session storage is disabled'); // eslint-disable-line no-console + isEnabled = false; + } + + // cookies + if (!navigator.cookieEnabled) { + console.warn('cookies are disabled'); // eslint-disable-line no-console + isEnabled = false; + } + + return isEnabled; +}; + StrictWrapper.propTypes = { children: PropTypes.node.isRequired, }; @@ -36,17 +70,23 @@ export default class StripesCore extends Component { constructor(props) { super(props); - const parsedTenant = getStoredTenant(); + if (isStorageEnabled()) { + const parsedTenant = getStoredTenant(); - const okapi = (typeof okapiConfig === 'object' && Object.keys(okapiConfig).length > 0) - ? { ...okapiConfig, tenant: parsedTenant?.tenantName || okapiConfig.tenant, clientId: parsedTenant?.clientId || okapiConfig.clientId } : { withoutOkapi: true }; + const okapi = (typeof okapiConfig === 'object' && Object.keys(okapiConfig).length > 0) + ? { ...okapiConfig, tenant: parsedTenant?.tenantName || okapiConfig.tenant, clientId: parsedTenant?.clientId || okapiConfig.clientId } : { withoutOkapi: true }; - const initialState = merge({}, { okapi }, props.initialState); + const initialState = merge({}, { okapi }, props.initialState); - this.logger = configureLogger(config); - this.epics = configureEpics(connectErrorEpic); - this.store = configureStore(initialState, this.logger, this.epics); - this.actionNames = gatherActions(); + this.logger = configureLogger(config); + this.epics = configureEpics(connectErrorEpic); + this.store = configureStore(initialState, this.logger, this.epics); + this.actionNames = gatherActions(); + + this.state = { isStorageEnabled: true }; + } else { + this.state = { isStorageEnabled: false }; + } } componentWillUnmount() { @@ -54,6 +94,13 @@ export default class StripesCore extends Component { } render() { + // Stripes requires cookies (for login) and session and local storage + // (for session state and all manner of things). If these are not enabled, + // stop and show an error message. + if (!this.state.isStorageEnabled) { + return ; + } + // no need to pass along `initialState` // eslint-disable-next-line no-unused-vars const { initialState, ...props } = this.props; diff --git a/src/App.test.js b/src/App.test.js new file mode 100644 index 00000000..eb805f6b --- /dev/null +++ b/src/App.test.js @@ -0,0 +1,36 @@ +import { isStorageEnabled } from './App'; + +const storageMock = () => ({ + getItem: () => { + throw new Error(); + }, +}); + +describe('isStorageEnabled', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('returns true when all storage options are enabled', () => { + expect(isStorageEnabled()).toBeTrue; + }); + + describe('returns false when any storage option is disabled', () => { + it('handles local storage', () => { + Object.defineProperty(window, 'localStorage', { value: storageMock }); + const isEnabled = isStorageEnabled(); + expect(isEnabled).toBeFalse; + }); + it('handles session storage', () => { + Object.defineProperty(window, 'sessionStorage', { value: storageMock }); + const isEnabled = isStorageEnabled(); + expect(isEnabled).toBeFalse; + }); + + it('handles cookies', () => { + jest.spyOn(navigator, 'cookieEnabled', 'get').mockReturnValue(false); + const isEnabled = isStorageEnabled(); + expect(isEnabled).toBeFalse; + }); + }); +}); diff --git a/src/components/AppConfigError/AppConfigError.css b/src/components/AppConfigError/AppConfigError.css new file mode 100644 index 00000000..e8cc7eff --- /dev/null +++ b/src/components/AppConfigError/AppConfigError.css @@ -0,0 +1,32 @@ +@import "@folio/stripes-components/lib/variables.css"; + +.wrapper { + display: flex; + justify-content: center; + min-height: 100vh; +} + +.container { + width: 100%; + max-width: 940px; + min-height: 330px; + margin: 12vh 2rem 0; +} + +@media (--medium-up) { + .container { + min-height: initial; + } +} + +@media (--large-up) { + .header { + font-size: var(--font-size-xx-large); + } +} + +@media (height <= 440px) { + .container { + min-height: 330px; + } +} diff --git a/src/components/AppConfigError/AppConfigError.js b/src/components/AppConfigError/AppConfigError.js new file mode 100644 index 00000000..bf3623bc --- /dev/null +++ b/src/components/AppConfigError/AppConfigError.js @@ -0,0 +1,47 @@ +import { branding } from 'stripes-config'; + +import { + Row, + Col, + Headline, +} from '@folio/stripes-components'; + +import OrganizationLogo from '../OrganizationLogo'; +import styles from './AppConfigError.css'; + +/** + * AppConfigError + * Show an error message. This component is rendered by App, before anything + * else, when it detects that local storage, session storage, or cookies are + * unavailable. This happens _before_ Root has been initialized, i.e. before + * an IntlProvider is available, hence the hard-coded, English-only message. + * + * @returns English-only error message + */ +const AppConfigError = () => { + return ( +
+
+
+ + + + + + + + + FOLIO requires cookies, sessionStorage, and localStorage. Please enable these features and try again. + + + +
+
+
+ ); +}; + +export default AppConfigError; diff --git a/src/components/AppConfigError/AppConfigError.test.js b/src/components/AppConfigError/AppConfigError.test.js new file mode 100644 index 00000000..95f88aa3 --- /dev/null +++ b/src/components/AppConfigError/AppConfigError.test.js @@ -0,0 +1,12 @@ +import { render, screen } from '@folio/jest-config-stripes/testing-library/react'; +import AppConfigError from './AppConfigError'; + +jest.mock('../OrganizationLogo', () => () => 'OrganizationLogo'); +describe('AppConfigError', () => { + it('displays a warning message', async () => { + render(); + + expect(screen.getByText(/cookies/i)).toBeInTheDocument(); + expect(screen.getByText(/storage/i)).toBeInTheDocument(); + }); +}); diff --git a/src/components/AppConfigError/index.js b/src/components/AppConfigError/index.js new file mode 100644 index 00000000..f6a6ec98 --- /dev/null +++ b/src/components/AppConfigError/index.js @@ -0,0 +1 @@ +export { default } from './AppConfigError'; diff --git a/src/components/OIDCRedirect.js b/src/components/OIDCRedirect.js index 9d463fe9..8898ca6e 100644 --- a/src/components/OIDCRedirect.js +++ b/src/components/OIDCRedirect.js @@ -5,7 +5,15 @@ import { getUnauthorizedPathFromSession, removeUnauthorizedPathFromSession } fro // Setting at top of component since value should be retained during re-renders // but will be correctly re-fetched when redirected from Keycloak login page. -const unauthorizedPath = getUnauthorizedPathFromSession(); +// The empty try/catch is necessary because, by setting this at the top of +// the component, it is automatically executed even before renders. +// IOW, even though we check for session-storage in App, we still have to +// protect the call here. +let unauthorizedPath = null; +try { + unauthorizedPath = getUnauthorizedPathFromSession(); +} catch (e) { // eslint-disable-line no-empty +} /** * OIDCRedirect authenticated route handler for /oidc-landing. From 792d3b2bc8021194b2f3ed1cbd89d8b2a6775589 Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Mon, 13 Jan 2025 14:51:02 -0500 Subject: [PATCH 4/4] STCOR-934 CVE-2024-47764 update react cookie (#1559) Update `react-cookie` to `^7.2.2` to receive updates in transitive deps and avoid CVE-2024-47764. Refs STCOR-934 --- package.json | 2 +- yarn.lock | 47 ++++++++++++++++++++++++++--------------------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index 05dc18b3..62ea54f2 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "ms": "^2.1.3", "prop-types": "^15.5.10", "query-string": "^7.1.2", - "react-cookie": "^4.0.3", + "react-cookie": "^7.2.2", "react-final-form": "^6.3.0", "react-query": "^3.6.0", "react-titled": "^2.0.0", diff --git a/yarn.lock b/yarn.lock index 781fb316..a270f02e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2841,16 +2841,16 @@ "@types/node" "*" "@types/responselike" "^1.0.0" -"@types/cookie@^0.3.3": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803" - integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow== - "@types/cookie@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== +"@types/cookie@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5" + integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA== + "@types/cors@^2.8.12": version "2.8.17" resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b" @@ -2893,7 +2893,7 @@ dependencies: "@types/node" "*" -"@types/hoist-non-react-statics@^3.0.1", "@types/hoist-non-react-statics@^3.3.1": +"@types/hoist-non-react-statics@^3.3.1", "@types/hoist-non-react-statics@^3.3.5": version "3.3.5" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz#dab7867ef789d87e2b4b0003c9d65c49cc44a494" integrity sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg== @@ -4942,7 +4942,12 @@ cookie@0.6.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== -cookie@^0.4.0, cookie@~0.4.1: +cookie@^0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== + +cookie@~0.4.1: version "0.4.2" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== @@ -7817,7 +7822,7 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -11377,14 +11382,14 @@ rc@1.2.8, rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-cookie@^4.0.3: - version "4.1.1" - resolved "https://registry.yarnpkg.com/react-cookie/-/react-cookie-4.1.1.tgz#832e134ad720e0de3e03deaceaab179c4061a19d" - integrity sha512-ffn7Y7G4bXiFbnE+dKhHhbP+b8I34mH9jqnm8Llhj89zF4nPxPutxHT1suUqMeCEhLDBI7InYwf1tpaSoK5w8A== +react-cookie@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/react-cookie/-/react-cookie-7.2.2.tgz#a7559e552ea9cca39a4b3686723a5acf504b8f84" + integrity sha512-e+hi6axHcw9VODoeVu8WyMWyoosa1pzpyjfvrLdF7CexfU+WSGZdDuRfHa4RJgTpfv3ZjdIpHE14HpYBieHFhg== dependencies: - "@types/hoist-non-react-statics" "^3.0.1" - hoist-non-react-statics "^3.0.0" - universal-cookie "^4.0.0" + "@types/hoist-non-react-statics" "^3.3.5" + hoist-non-react-statics "^3.3.2" + universal-cookie "^7.0.0" react-dom@^18.2.0: version "18.2.0" @@ -13361,13 +13366,13 @@ unique-string@^3.0.0: dependencies: crypto-random-string "^4.0.0" -universal-cookie@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/universal-cookie/-/universal-cookie-4.0.4.tgz#06e8b3625bf9af049569ef97109b4bb226ad798d" - integrity sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw== +universal-cookie@^7.0.0: + version "7.2.2" + resolved "https://registry.yarnpkg.com/universal-cookie/-/universal-cookie-7.2.2.tgz#93ae9ec55baab89b24300473543170bb8112773c" + integrity sha512-fMiOcS3TmzP2x5QV26pIH3mvhexLIT0HmPa3V7Q7knRfT9HG6kTwq02HZGLPw0sAOXrAmotElGRvTLCMbJsvxQ== dependencies: - "@types/cookie" "^0.3.3" - cookie "^0.4.0" + "@types/cookie" "^0.6.0" + cookie "^0.7.2" universal-user-agent@^6.0.0: version "6.0.1"