From d33b65dc52242c5211f3bda3578b976341e8522d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 23 Dec 2021 10:50:16 +0100 Subject: [PATCH 01/24] create-app: remove unused serve_index.html Signed-off-by: Patrik Oldsberg --- .changeset/happy-moose-worry.md | 5 ++++ .../create-app/templates/serve_index.html | 27 ------------------- 2 files changed, 5 insertions(+), 27 deletions(-) create mode 100644 .changeset/happy-moose-worry.md delete mode 100644 packages/create-app/templates/serve_index.html diff --git a/.changeset/happy-moose-worry.md b/.changeset/happy-moose-worry.md new file mode 100644 index 0000000000..66f0728f7d --- /dev/null +++ b/.changeset/happy-moose-worry.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Removed unused templating asset. diff --git a/packages/create-app/templates/serve_index.html b/packages/create-app/templates/serve_index.html deleted file mode 100644 index d7aebb7f4b..0000000000 --- a/packages/create-app/templates/serve_index.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - Backstage - - - -
- - - From 94e731c173e8b79d13f4d49dba925b8a1d54c087 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 23 Dec 2021 10:57:01 +0100 Subject: [PATCH 02/24] yarn.lock: bump @types/node-fetch Signed-off-by: Patrik Oldsberg --- yarn.lock | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/yarn.lock b/yarn.lock index fe8a92d1a5..c9efb72cd1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7847,15 +7847,7 @@ resolved "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.0.tgz#f946cefb5c05c64f460090f6be97bd50460c8898" integrity sha512-RNBIyVwa/1v2r8/SqK8tadH2sJlFRAo5Ghac/cOcCv4Kp94m0I03UmAh9WVhCqS9ZdB84dF3x47p9aTw8E4c4A== -"@types/node-fetch@^2.5.0", "@types/node-fetch@^2.5.7": - version "2.5.8" - resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.8.tgz#e199c835d234c7eb0846f6618012e558544ee2fb" - integrity sha512-fbjI6ja0N5ZA8TV53RUqzsKNkl9fv8Oj3T7zxW7FGv1GSH7gwJaNF8dzCjrqKaxKeUpTz4yT1DaJFq/omNpGfw== - dependencies: - "@types/node" "*" - form-data "^3.0.0" - -"@types/node-fetch@^2.5.12": +"@types/node-fetch@^2.5.0", "@types/node-fetch@^2.5.12", "@types/node-fetch@^2.5.7": version "2.5.12" resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz#8a6f779b1d4e60b7a57fb6fd48d84fb545b9cc66" integrity sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw== From 8b532a6c02d7148f49569237cafd9e9619e76adc Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 27 Dec 2021 18:40:20 +0100 Subject: [PATCH 03/24] Initial SearchTypeFacet implementation. Signed-off-by: Eric Peterson --- .changeset/search-en-jul-hemma.md | 7 + .../app/src/components/search/SearchPage.tsx | 33 +++- plugins/search/api-report.md | 14 ++ .../SearchTypeFacet.stories.tsx | 49 +++++ .../SearchTypeFacet/SearchTypeFacet.tsx | 184 ++++++++++++++++++ .../src/components/SearchTypeFacet/index.ts | 18 ++ plugins/search/src/components/index.tsx | 1 + plugins/search/src/index.ts | 2 + 8 files changed, 301 insertions(+), 7 deletions(-) create mode 100644 .changeset/search-en-jul-hemma.md create mode 100644 plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.stories.tsx create mode 100644 plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.tsx create mode 100644 plugins/search/src/components/SearchTypeFacet/index.ts diff --git a/.changeset/search-en-jul-hemma.md b/.changeset/search-en-jul-hemma.md new file mode 100644 index 0000000000..a2eee15e7c --- /dev/null +++ b/.changeset/search-en-jul-hemma.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-search': patch +--- + +Introduces a `` component, which operates on the same part of a search query as the `` component, but in a more opinionated way (as a single-select control surface suitable for faceted search UIs). + +Check the [search plugin storybook](https://backstage.io/storybook/?path=/story/plugins-search-searchtypefacet--default) to see how it can be used. diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx index 54f59d1a6e..e8d590bc92 100644 --- a/packages/app/src/components/search/SearchPage.tsx +++ b/packages/app/src/components/search/SearchPage.tsx @@ -14,7 +14,14 @@ * limitations under the License. */ -import { Content, Header, Lifecycle, Page } from '@backstage/core-components'; +import { + CatalogIcon, + Content, + DocsIcon, + Header, + Lifecycle, + Page, +} from '@backstage/core-components'; import { CatalogResultListItem } from '@backstage/plugin-catalog'; import { DefaultResultListItem, @@ -22,7 +29,7 @@ import { SearchFilter, SearchResult, SearchResultPager, - SearchType, + SearchTypeFacet, } from '@backstage/plugin-search'; import { DocsResultListItem } from '@backstage/plugin-techdocs'; import { Grid, List, makeStyles, Paper, Theme } from '@material-ui/core'; @@ -39,6 +46,7 @@ const useStyles = makeStyles((theme: Theme) => ({ }, filters: { padding: theme.spacing(2), + marginTop: theme.spacing(2), }, })); @@ -55,12 +63,23 @@ const SearchPage = () => { + , + }, + { + value: 'techdocs', + name: 'Documentation', + icon: , + }, + ]} + /> - JSX.Element; +// @public +export const SearchTypeFacet: (props: SearchTypeFacetProps) => JSX.Element; + +// @public (undocumented) +export type SearchTypeFacetProps = { + name: string; + types: Array<{ + value: string; + name: string; + icon: JSX.Element; + }>; + defaultValue?: string; +}; + // Warning: (ae-missing-release-tag) "SidebarSearch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.stories.tsx b/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.stories.tsx new file mode 100644 index 0000000000..c83f3d88d7 --- /dev/null +++ b/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.stories.tsx @@ -0,0 +1,49 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useState } from 'react'; +import CatalogIcon from '@material-ui/icons/MenuBook'; +import DocsIcon from '@material-ui/icons/Description'; + +import { SearchTypeFacet } from '../index'; +import { SearchContext } from '../SearchContext'; + +export default { + title: 'Plugins/Search/SearchTypeFacet', + component: SearchTypeFacet, +}; + +export const Default = () => { + const [types, setTypes] = useState([]); + + return ( + {} } as any} + > + , + }, + { value: 'techdocs', name: 'Documentation', icon: }, + ]} + /> + + ); +}; diff --git a/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.tsx b/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.tsx new file mode 100644 index 0000000000..e054e0d028 --- /dev/null +++ b/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.tsx @@ -0,0 +1,184 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { + ChangeEvent, + cloneElement, + Fragment, + useEffect, + useState, +} from 'react'; +import { useSearch } from '../SearchContext'; +import { + Accordion, + AccordionSummary, + AccordionDetails, + Card, + CardContent, + CardHeader, + Divider, + List, + ListItem, + ListItemIcon, + ListItemText, + makeStyles, +} from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import AllIcon from '@material-ui/icons/FontDownload'; + +const useStyles = makeStyles(theme => ({ + card: { + backgroundColor: 'rgba(0, 0, 0, .11)', + }, + cardContent: { + paddingTop: theme.spacing(1), + }, + icon: { + color: theme.palette.common.black, + }, + list: { + width: '100%', + }, + listItemIcon: { + width: '24px', + height: '24px', + }, + accordion: { + backgroundColor: theme.palette.background.paper, + }, + accordionSummary: { + minHeight: 'auto', + '&.Mui-expanded': { + minHeight: 'auto', + }, + }, + accordionSummaryContent: { + margin: theme.spacing(2, 0), + '&.Mui-expanded': { + margin: theme.spacing(2, 0), + }, + }, + accordionDetails: { + padding: theme.spacing(0, 0, 1), + }, +})); + +/** + * @public + */ +export type SearchTypeFacetProps = { + /* what about this? */ + name: string; + types: Array<{ + value: string; + name: string; + icon: JSX.Element; + }>; + defaultValue?: string; +}; + +/** + * A control surface for the search query's "types" property, displayed as a + * single-select collapsible accordion suitable for use in faceted search UIs. + * @public + */ +export const SearchTypeFacet = (props: SearchTypeFacetProps) => { + const classes = useStyles(); + const { setPageCursor, setTypes, types } = useSearch(); + const [expanded, setExpanded] = useState(true); + const { defaultValue, name, types: givenTypes } = props; + + const handleChange = (_event: ChangeEvent<{}>, newExpanded: boolean) => + setExpanded(newExpanded); + const handleClick = (type: string) => { + return () => { + setTypes(type !== '' ? [type] : []); + setPageCursor('0'); + setExpanded(false); + }; + }; + + // Handle any provided defaultValue + useEffect(() => { + if (defaultValue) { + setTypes([defaultValue]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const definedTypes = [ + { + value: '', + name: 'All', + icon: , + }, + ...givenTypes, + ]; + const selected = types[0] || ''; + + return ( + + + + + } + IconButtonProps={{ size: 'small' }} + > + {expanded + ? 'Collapse' + : definedTypes.filter(t => t.value === selected)[0]!.name} + + + + {definedTypes.map(type => ( + + + + + {cloneElement(type.icon, { + className: classes.listItemIcon, + })} + + + + + ))} + + + + + + ); +}; diff --git a/plugins/search/src/components/SearchTypeFacet/index.ts b/plugins/search/src/components/SearchTypeFacet/index.ts new file mode 100644 index 0000000000..f98c6f8326 --- /dev/null +++ b/plugins/search/src/components/SearchTypeFacet/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { SearchTypeFacet } from './SearchTypeFacet'; +export type { SearchTypeFacetProps } from './SearchTypeFacet'; diff --git a/plugins/search/src/components/index.tsx b/plugins/search/src/components/index.tsx index e413e9505d..82cc7bf762 100644 --- a/plugins/search/src/components/index.tsx +++ b/plugins/search/src/components/index.tsx @@ -24,6 +24,7 @@ export * from './SearchPage'; export * from './SearchResult'; export * from './SearchResultPager'; export * from './SearchType'; +export * from './SearchTypeFacet'; export * from './SidebarSearch'; export * from './SidebarSearchModal'; export * from './HomePageComponent'; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index 5bfdcf4318..8f760f42db 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -34,6 +34,7 @@ export { SearchPage as Router, SearchResultPager, SearchType, + SearchTypeFacet, SidebarSearch, useSearch, } from './components'; @@ -45,6 +46,7 @@ export type { FiltersState, SearchBarProps, SearchBarBaseProps, + SearchTypeFacetProps, } from './components'; export { DefaultResultListItem, From 627b98ac0771342e8a4f686b8b5893e76260f7d7 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 28 Dec 2021 11:34:03 +0100 Subject: [PATCH 04/24] -> Signed-off-by: Eric Peterson --- .changeset/search-en-jul-hemma.md | 4 +- .../app/src/components/search/SearchPage.tsx | 4 +- plugins/search/api-report.md | 24 ++++----- .../SearchType.Accordion.tsx} | 30 ++++-------- .../SearchType/SearchType.stories.tsx | 22 +++++++++ .../src/components/SearchType/SearchType.tsx | 25 +++++++--- .../search/src/components/SearchType/index.ts | 1 + .../SearchTypeFacet.stories.tsx | 49 ------------------- .../src/components/SearchTypeFacet/index.ts | 18 ------- plugins/search/src/components/index.tsx | 1 - plugins/search/src/index.ts | 4 +- 11 files changed, 71 insertions(+), 111 deletions(-) rename plugins/search/src/components/{SearchTypeFacet/SearchTypeFacet.tsx => SearchType/SearchType.Accordion.tsx} (87%) delete mode 100644 plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.stories.tsx delete mode 100644 plugins/search/src/components/SearchTypeFacet/index.ts diff --git a/.changeset/search-en-jul-hemma.md b/.changeset/search-en-jul-hemma.md index a2eee15e7c..9fae9fcaa5 100644 --- a/.changeset/search-en-jul-hemma.md +++ b/.changeset/search-en-jul-hemma.md @@ -2,6 +2,6 @@ '@backstage/plugin-search': patch --- -Introduces a `` component, which operates on the same part of a search query as the `` component, but in a more opinionated way (as a single-select control surface suitable for faceted search UIs). +Introduces a `` variant, which operates on the same part of a search query as the existing ``, but in a more opinionated way (as a single-select control surface suitable for faceted search UIs). -Check the [search plugin storybook](https://backstage.io/storybook/?path=/story/plugins-search-searchtypefacet--default) to see how it can be used. +Check the [search plugin storybook](https://backstage.io/storybook/?path=/story/plugins-search-searchtype--accordion) to see how it can be used. diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx index e8d590bc92..c99bb78c84 100644 --- a/packages/app/src/components/search/SearchPage.tsx +++ b/packages/app/src/components/search/SearchPage.tsx @@ -29,7 +29,7 @@ import { SearchFilter, SearchResult, SearchResultPager, - SearchTypeFacet, + SearchType, } from '@backstage/plugin-search'; import { DocsResultListItem } from '@backstage/plugin-techdocs'; import { Grid, List, makeStyles, Paper, Theme } from '@material-ui/core'; @@ -63,7 +63,7 @@ const SearchPage = () => { - JSX.Element; -// Warning: (ae-forgotten-export) The symbol "SearchTypeProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "SearchType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const SearchType: ({ - values, - className, - name, - defaultValue, -}: SearchTypeProps) => JSX.Element; - -// @public -export const SearchTypeFacet: (props: SearchTypeFacetProps) => JSX.Element; +export const SearchType: { + (props: SearchTypeProps): JSX.Element; + Accordion(props: SearchTypeAccordionProps): JSX.Element; +}; // @public (undocumented) -export type SearchTypeFacetProps = { +export type SearchTypeAccordionProps = { name: string; types: Array<{ value: string; @@ -238,6 +232,14 @@ export type SearchTypeFacetProps = { defaultValue?: string; }; +// @public (undocumented) +export type SearchTypeProps = { + className?: string; + name: string; + values?: string[]; + defaultValue?: string[] | string | null; +}; + // Warning: (ae-missing-release-tag) "SidebarSearch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.tsx b/plugins/search/src/components/SearchType/SearchType.Accordion.tsx similarity index 87% rename from plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.tsx rename to plugins/search/src/components/SearchType/SearchType.Accordion.tsx index e054e0d028..9ad03db8a1 100644 --- a/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Accordion.tsx @@ -14,13 +14,7 @@ * limitations under the License. */ -import React, { - ChangeEvent, - cloneElement, - Fragment, - useEffect, - useState, -} from 'react'; +import React, { cloneElement, Fragment, useEffect, useState } from 'react'; import { useSearch } from '../SearchContext'; import { Accordion, @@ -79,8 +73,7 @@ const useStyles = makeStyles(theme => ({ /** * @public */ -export type SearchTypeFacetProps = { - /* what about this? */ +export type SearchTypeAccordionProps = { name: string; types: Array<{ value: string; @@ -90,23 +83,17 @@ export type SearchTypeFacetProps = { defaultValue?: string; }; -/** - * A control surface for the search query's "types" property, displayed as a - * single-select collapsible accordion suitable for use in faceted search UIs. - * @public - */ -export const SearchTypeFacet = (props: SearchTypeFacetProps) => { +export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => { const classes = useStyles(); const { setPageCursor, setTypes, types } = useSearch(); const [expanded, setExpanded] = useState(true); const { defaultValue, name, types: givenTypes } = props; - const handleChange = (_event: ChangeEvent<{}>, newExpanded: boolean) => - setExpanded(newExpanded); + const toggleExpanded = () => setExpanded(prevState => !prevState); const handleClick = (type: string) => { return () => { setTypes(type !== '' ? [type] : []); - setPageCursor('0'); + setPageCursor(''); setExpanded(false); }; }; @@ -136,7 +123,7 @@ export const SearchTypeFacet = (props: SearchTypeFacetProps) => { { diff --git a/plugins/search/src/components/SearchType/SearchType.stories.tsx b/plugins/search/src/components/SearchType/SearchType.stories.tsx index da662d8fef..d458c59aad 100644 --- a/plugins/search/src/components/SearchType/SearchType.stories.tsx +++ b/plugins/search/src/components/SearchType/SearchType.stories.tsx @@ -14,6 +14,9 @@ * limitations under the License. */ import React, { useState } from 'react'; +import CatalogIcon from '@material-ui/icons/MenuBook'; +import DocsIcon from '@material-ui/icons/Description'; +import UsersGroupsIcon from '@material-ui/icons/Person'; import { SearchType } from '../index'; import { SearchContext } from '../SearchContext'; @@ -34,3 +37,22 @@ export const Default = () => { ); }; + +export const Accordion = () => { + const [types, setTypes] = useState([]); + const setPageCursor = () => {}; + + return ( + + }, + { value: 'value-2', name: 'Value Two', icon: }, + { value: 'value-3', name: 'Value Three', icon: }, + ]} + /> + + ); +}; diff --git a/plugins/search/src/components/SearchType/SearchType.tsx b/plugins/search/src/components/SearchType/SearchType.tsx index 5290caa0b7..f42802819a 100644 --- a/plugins/search/src/components/SearchType/SearchType.tsx +++ b/plugins/search/src/components/SearchType/SearchType.tsx @@ -25,6 +25,10 @@ import { } from '@material-ui/core'; import React, { ChangeEvent } from 'react'; import { useEffectOnce } from 'react-use'; +import { + SearchTypeAccordion, + SearchTypeAccordionProps, +} from './SearchType.Accordion'; import { useSearch } from '../SearchContext'; const useStyles = makeStyles(theme => ({ @@ -41,6 +45,9 @@ const useStyles = makeStyles(theme => ({ }, })); +/** + * @public + */ export type SearchTypeProps = { className?: string; name: string; @@ -48,12 +55,8 @@ export type SearchTypeProps = { defaultValue?: string[] | string | null; }; -const SearchType = ({ - values = [], - className, - name, - defaultValue, -}: SearchTypeProps) => { +const SearchType = (props: SearchTypeProps) => { + const { className, defaultValue, name, values = [] } = props; const classes = useStyles(); const { types, setTypes } = useSearch(); @@ -112,4 +115,14 @@ const SearchType = ({ ); }; +/** + * A control surface for the search query's "types" property, displayed as a + * single-select collapsible accordion suitable for use in faceted search UIs. + * @public + */ +SearchType.Accordion = (props: SearchTypeAccordionProps) => { + return ; +}; + export { SearchType }; +export type { SearchTypeAccordionProps }; diff --git a/plugins/search/src/components/SearchType/index.ts b/plugins/search/src/components/SearchType/index.ts index 400f3d2a46..e605276455 100644 --- a/plugins/search/src/components/SearchType/index.ts +++ b/plugins/search/src/components/SearchType/index.ts @@ -15,3 +15,4 @@ */ export { SearchType } from './SearchType'; +export type { SearchTypeAccordionProps, SearchTypeProps } from './SearchType'; diff --git a/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.stories.tsx b/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.stories.tsx deleted file mode 100644 index c83f3d88d7..0000000000 --- a/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.stories.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React, { useState } from 'react'; -import CatalogIcon from '@material-ui/icons/MenuBook'; -import DocsIcon from '@material-ui/icons/Description'; - -import { SearchTypeFacet } from '../index'; -import { SearchContext } from '../SearchContext'; - -export default { - title: 'Plugins/Search/SearchTypeFacet', - component: SearchTypeFacet, -}; - -export const Default = () => { - const [types, setTypes] = useState([]); - - return ( - {} } as any} - > - , - }, - { value: 'techdocs', name: 'Documentation', icon: }, - ]} - /> - - ); -}; diff --git a/plugins/search/src/components/SearchTypeFacet/index.ts b/plugins/search/src/components/SearchTypeFacet/index.ts deleted file mode 100644 index f98c6f8326..0000000000 --- a/plugins/search/src/components/SearchTypeFacet/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { SearchTypeFacet } from './SearchTypeFacet'; -export type { SearchTypeFacetProps } from './SearchTypeFacet'; diff --git a/plugins/search/src/components/index.tsx b/plugins/search/src/components/index.tsx index 82cc7bf762..e413e9505d 100644 --- a/plugins/search/src/components/index.tsx +++ b/plugins/search/src/components/index.tsx @@ -24,7 +24,6 @@ export * from './SearchPage'; export * from './SearchResult'; export * from './SearchResultPager'; export * from './SearchType'; -export * from './SearchTypeFacet'; export * from './SidebarSearch'; export * from './SidebarSearchModal'; export * from './HomePageComponent'; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index 8f760f42db..5f0008c425 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -34,7 +34,6 @@ export { SearchPage as Router, SearchResultPager, SearchType, - SearchTypeFacet, SidebarSearch, useSearch, } from './components'; @@ -46,7 +45,8 @@ export type { FiltersState, SearchBarProps, SearchBarBaseProps, - SearchTypeFacetProps, + SearchTypeAccordionProps, + SearchTypeProps, } from './components'; export { DefaultResultListItem, From 6d8e3a96513bcf231f5087c3ba8c1f67bcc852ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 22 Dec 2021 12:35:23 +0100 Subject: [PATCH 05/24] Internal cleanup of the exports structure in the search plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/four-waves-tan.md | 5 +++ .../DefaultResultListItem.stories.tsx | 6 +-- .../LegacySearchPage/LegacySearchPage.tsx | 1 + .../SearchBar/SearchBar.stories.tsx | 5 ++- .../SearchFilter/SearchFilter.stories.tsx | 5 ++- .../SearchModal/SearchModal.stories.tsx | 8 ++-- .../SearchModal/SearchModal.test.tsx | 1 + .../components/SearchModal/SearchModal.tsx | 1 + .../components/SearchPage/SearchPage.test.tsx | 2 +- .../SearchResult/SearchResult.stories.tsx | 10 +++-- .../SearchType/SearchType.stories.tsx | 4 +- plugins/search/src/components/index.tsx | 29 -------------- plugins/search/src/index.ts | 40 +++++++++---------- 13 files changed, 48 insertions(+), 69 deletions(-) create mode 100644 .changeset/four-waves-tan.md delete mode 100644 plugins/search/src/components/index.tsx diff --git a/.changeset/four-waves-tan.md b/.changeset/four-waves-tan.md new file mode 100644 index 0000000000..444891d31b --- /dev/null +++ b/.changeset/four-waves-tan.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search': patch +--- + +Internal cleanup of the exports structure diff --git a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx index 0445a35943..baf00767db 100644 --- a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx +++ b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx @@ -14,13 +14,13 @@ * limitations under the License. */ -import React from 'react'; +import { Button } from '@backstage/core-components'; import { Grid } from '@material-ui/core'; import FindInPageIcon from '@material-ui/icons/FindInPage'; import GroupIcon from '@material-ui/icons/Group'; -import { Button } from '@backstage/core-components'; -import { DefaultResultListItem } from '../index'; +import React from 'react'; import { MemoryRouter } from 'react-router'; +import { DefaultResultListItem } from './DefaultResultListItem'; export default { title: 'Plugins/Search/DefaultResultListItem', diff --git a/plugins/search/src/components/LegacySearchPage/LegacySearchPage.tsx b/plugins/search/src/components/LegacySearchPage/LegacySearchPage.tsx index c8f2e538cb..ec0fc3acb9 100644 --- a/plugins/search/src/components/LegacySearchPage/LegacySearchPage.tsx +++ b/plugins/search/src/components/LegacySearchPage/LegacySearchPage.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Grid } from '@material-ui/core'; import React, { useEffect, useState } from 'react'; import { useDebounce } from 'react-use'; diff --git a/plugins/search/src/components/SearchBar/SearchBar.stories.tsx b/plugins/search/src/components/SearchBar/SearchBar.stories.tsx index d00f391bc7..8d81e15e56 100644 --- a/plugins/search/src/components/SearchBar/SearchBar.stories.tsx +++ b/plugins/search/src/components/SearchBar/SearchBar.stories.tsx @@ -14,10 +14,11 @@ * limitations under the License. */ +import { Grid, makeStyles, Paper } from '@material-ui/core'; import React from 'react'; -import { Paper, Grid, makeStyles } from '@material-ui/core'; -import { SearchBar, SearchContext } from '../index'; import { MemoryRouter } from 'react-router'; +import { SearchContext } from '../SearchContext'; +import { SearchBar } from './SearchBar'; export default { title: 'Plugins/Search/SearchBar', diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx b/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx index 190856fca8..c98c1106e6 100644 --- a/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx +++ b/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx @@ -14,10 +14,11 @@ * limitations under the License. */ -import React from 'react'; import { Grid, Paper } from '@material-ui/core'; -import { SearchFilter, SearchContext } from '../index'; +import React from 'react'; import { MemoryRouter } from 'react-router'; +import { SearchContext } from '../SearchContext'; +import { SearchFilter } from './SearchFilter'; export default { title: 'Plugins/Search/SearchFilter', diff --git a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx index 6fa93c6588..c0a774ab46 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx @@ -14,14 +14,14 @@ * limitations under the License. */ -import React, { ComponentType } from 'react'; -import { Button } from '@material-ui/core'; import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { wrapInTestApp } from '@backstage/test-utils'; -import { SearchModal } from '../index'; -import { useSearch, SearchContextProvider } from '../SearchContext'; +import { Button } from '@material-ui/core'; +import React, { ComponentType } from 'react'; import { searchApiRef } from '../../apis'; import { rootRouteRef } from '../../plugin'; +import { SearchContextProvider, useSearch } from '../SearchContext'; +import { SearchModal } from './SearchModal'; const mockSearchApi = { query: () => diff --git a/plugins/search/src/components/SearchModal/SearchModal.test.tsx b/plugins/search/src/components/SearchModal/SearchModal.test.tsx index ff9622dfb9..d3209f9a9b 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.test.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.test.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { screen } from '@testing-library/react'; import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; diff --git a/plugins/search/src/components/SearchModal/SearchModal.tsx b/plugins/search/src/components/SearchModal/SearchModal.tsx index ef1eaf650a..8b4da51592 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { Dialog, diff --git a/plugins/search/src/components/SearchPage/SearchPage.test.tsx b/plugins/search/src/components/SearchPage/SearchPage.test.tsx index c9980b3bf7..9486196584 100644 --- a/plugins/search/src/components/SearchPage/SearchPage.test.tsx +++ b/plugins/search/src/components/SearchPage/SearchPage.test.tsx @@ -18,7 +18,7 @@ import { renderInTestApp } from '@backstage/test-utils'; import React from 'react'; import { useLocation, useOutlet } from 'react-router'; import { useSearch } from '../SearchContext'; -import { SearchPage } from './'; +import { SearchPage } from './SearchPage'; jest.mock('react-router', () => ({ ...jest.requireActual('react-router'), diff --git a/plugins/search/src/components/SearchResult/SearchResult.stories.tsx b/plugins/search/src/components/SearchResult/SearchResult.stories.tsx index aebaecb8b4..82e4fc2916 100644 --- a/plugins/search/src/components/SearchResult/SearchResult.stories.tsx +++ b/plugins/search/src/components/SearchResult/SearchResult.stories.tsx @@ -14,11 +14,13 @@ * limitations under the License. */ -import React from 'react'; -import { List, ListItem } from '@material-ui/core'; -import { SearchResult, SearchContext, DefaultResultListItem } from '../index'; -import { MemoryRouter } from 'react-router'; import { Link } from '@backstage/core-components'; +import { List, ListItem } from '@material-ui/core'; +import React from 'react'; +import { MemoryRouter } from 'react-router'; +import { DefaultResultListItem } from '../DefaultResultListItem'; +import { SearchContext } from '../SearchContext'; +import { SearchResult } from './SearchResult'; export default { title: 'Plugins/Search/SearchResult', diff --git a/plugins/search/src/components/SearchType/SearchType.stories.tsx b/plugins/search/src/components/SearchType/SearchType.stories.tsx index da662d8fef..29dd66a3a6 100644 --- a/plugins/search/src/components/SearchType/SearchType.stories.tsx +++ b/plugins/search/src/components/SearchType/SearchType.stories.tsx @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useState } from 'react'; -import { SearchType } from '../index'; +import React, { useState } from 'react'; import { SearchContext } from '../SearchContext'; +import { SearchType } from './SearchType'; export default { title: 'Plugins/Search/SearchType', diff --git a/plugins/search/src/components/index.tsx b/plugins/search/src/components/index.tsx deleted file mode 100644 index e413e9505d..0000000000 --- a/plugins/search/src/components/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export * from './DefaultResultListItem'; -export * from './Filters'; -export * from './SearchBar'; -export * from './SearchContext'; -export * from './SearchFilter'; -export * from './SearchModal'; -export * from './SearchPage'; -export * from './SearchResult'; -export * from './SearchResultPager'; -export * from './SearchType'; -export * from './SidebarSearch'; -export * from './SidebarSearchModal'; -export * from './HomePageComponent'; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index 5bfdcf4318..fc0a149c88 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -22,30 +22,26 @@ export { searchApiRef } from './apis'; export type { SearchApi } from './apis'; -export { - Filters, - FiltersButton, - SearchBar, - SearchBarBase, - SearchContextProvider, - SearchFilter, - SearchFilterNext, - SearchModal, - SearchPage as Router, - SearchResultPager, - SearchType, - SidebarSearch, - useSearch, -} from './components'; + +export { Filters, FiltersButton } from './components/Filters'; +export type { FiltersState } from './components/Filters'; +export type { HomePageSearchBarProps } from './components/HomePageComponent'; +export { SearchBar, SearchBarBase } from './components/SearchBar'; export type { - SearchModalProps, - SidebarSearchModalProps, - HomePageSearchBarProps, - SidebarSearchProps, - FiltersState, - SearchBarProps, SearchBarBaseProps, -} from './components'; + SearchBarProps, +} from './components/SearchBar'; +export { SearchContextProvider, useSearch } from './components/SearchContext'; +export { SearchFilter, SearchFilterNext } from './components/SearchFilter'; +export { SearchModal } from './components/SearchModal'; +export type { SearchModalProps } from './components/SearchModal'; +export { SearchPage as Router } from './components/SearchPage'; +export { SearchResultPager } from './components/SearchResultPager'; +export { SearchType } from './components/SearchType'; +export { SidebarSearch } from './components/SidebarSearch'; +export type { SidebarSearchProps } from './components/SidebarSearch'; +export type { SidebarSearchModalProps } from './components/SidebarSearchModal'; + export { DefaultResultListItem, HomePageSearchBar, From b29d97633061f0bd1a067afdbeda18a651493235 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 28 Dec 2021 14:35:11 +0100 Subject: [PATCH 06/24] auth-backend: refactor auth0 to use sign-in resolver Signed-off-by: Patrik Oldsberg --- .../src/providers/auth0/provider.ts | 153 ++++++++++++++---- plugins/auth-backend/src/providers/index.ts | 7 +- 2 files changed, 122 insertions(+), 38 deletions(-) diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index 7aa98c3e65..4293e3e481 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -36,7 +36,15 @@ import { makeProfileInfo, PassportDoneCallback, } from '../../lib/passport'; -import { RedirectInfo, AuthProviderFactory } from '../types'; +import { + RedirectInfo, + AuthProviderFactory, + AuthHandler, + SignInResolver, +} from '../types'; +import { CatalogIdentityClient } from '../../lib/catalog'; +import { TokenIssuer } from '../../identity'; +import { Logger } from 'winston'; type PrivateInfo = { refreshToken: string; @@ -44,12 +52,27 @@ type PrivateInfo = { export type Auth0AuthProviderOptions = OAuthProviderOptions & { domain: string; + signInResolver?: SignInResolver; + authHandler: AuthHandler; + tokenIssuer: TokenIssuer; + catalogIdentityClient: CatalogIdentityClient; + logger: Logger; }; export class Auth0AuthProvider implements OAuthHandlers { private readonly _strategy: Auth0Strategy; + private readonly signInResolver?: SignInResolver; + private readonly authHandler: AuthHandler; + private readonly tokenIssuer: TokenIssuer; + private readonly catalogIdentityClient: CatalogIdentityClient; + private readonly logger: Logger; constructor(options: Auth0AuthProviderOptions) { + this.signInResolver = options.signInResolver; + this.authHandler = options.authHandler; + this.tokenIssuer = options.tokenIssuer; + this.catalogIdentityClient = options.catalogIdentityClient; + this.logger = options.logger; this._strategy = new Auth0Strategy( { clientID: options.clientId, @@ -98,18 +121,8 @@ export class Auth0AuthProvider implements OAuthHandlers { PrivateInfo >(req, this._strategy); - const profile = makeProfileInfo(result.fullProfile, result.params.id_token); - return { - response: await this.populateIdentity({ - profile, - providerInfo: { - idToken: result.params.id_token, - accessToken: result.accessToken, - scope: result.params.scope, - expiresInSeconds: result.params.expires_in, - }, - }), + response: await this.handleResult(result), refreshToken: privateInfo.refreshToken, }; } @@ -125,53 +138,123 @@ export class Auth0AuthProvider implements OAuthHandlers { this._strategy, accessToken, ); - const profile = makeProfileInfo(fullProfile, params.id_token); - return this.populateIdentity({ - providerInfo: { - accessToken, - idToken: params.id_token, - expiresInSeconds: params.expires_in, - scope: params.scope, - }, - profile, + return this.handleResult({ + fullProfile, + params, + accessToken, + refreshToken: req.refreshToken, }); } - // Use this function to grab the user profile info from the token - // Then populate the profile with it - private async populateIdentity( - response: OAuthResponse, - ): Promise { - const { profile } = response; + private async handleResult(result: OAuthResult) { + const { profile } = await this.authHandler(result); - if (!profile.email) { - throw new Error('Profile does not contain an email'); + const response: OAuthResponse = { + providerInfo: { + idToken: result.params.id_token, + accessToken: result.accessToken, + scope: result.params.scope, + expiresInSeconds: result.params.expires_in, + }, + profile, + }; + + if (this.signInResolver) { + response.backstageIdentity = await this.signInResolver( + { + result, + profile, + }, + { + tokenIssuer: this.tokenIssuer, + catalogIdentityClient: this.catalogIdentityClient, + logger: this.logger, + }, + ); } - const id = profile.email.split('@')[0]; - - return { ...response, backstageIdentity: { id, token: '' } }; + return response; } } -export type Auth0ProviderOptions = {}; +const defaultSignInResolver: SignInResolver = async ( + info, + ctx, +) => { + const { profile } = info; + + if (!profile.email) { + throw new Error('Profile does not contain an email'); + } + + const id = profile.email.split('@')[0]; + + const token = await ctx.tokenIssuer.issueToken({ + claims: { sub: id, ent: [`user:default/${id}`] }, + }); + + return { id, token }; +}; + +export type Auth0ProviderOptions = { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; + + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; +}; export const createAuth0Provider = ( - _options?: Auth0ProviderOptions, + options?: Auth0ProviderOptions, ): AuthProviderFactory => { - return ({ providerId, globalConfig, config, tokenIssuer }) => + return ({ + providerId, + globalConfig, + config, + tokenIssuer, + catalogApi, + logger, + }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); const domain = envConfig.getString('domain'); const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const catalogIdentityClient = new CatalogIdentityClient({ + catalogApi, + tokenIssuer, + }); + + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile, params.id_token), + }); + + const signInResolver = options?.signIn?.resolver ?? defaultSignInResolver; + const provider = new Auth0AuthProvider({ clientId, clientSecret, callbackUrl, domain, + authHandler, + signInResolver, + tokenIssuer, + catalogIdentityClient, + logger, }); return OAuthAdapter.fromConfig(globalConfig, provider, { diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 9589e97265..aba41bdb73 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +export * from './atlassian'; +export * from './auth0'; +export * from './aws-alb'; +export * from './bitbucket'; export * from './github'; export * from './gitlab'; export * from './google'; @@ -21,9 +25,6 @@ export * from './microsoft'; export * from './oauth2'; export * from './oidc'; export * from './okta'; -export * from './bitbucket'; -export * from './atlassian'; -export * from './aws-alb'; export * from './saml'; export { factories as defaultAuthProviderFactories } from './factories'; From 04b1d4be44ee435dd138835db90a1e0761fb5c2d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 28 Dec 2021 15:26:36 +0100 Subject: [PATCH 07/24] auth-backend: refactor onelogin to use sign-in resolver Signed-off-by: Patrik Oldsberg --- plugins/auth-backend/src/providers/index.ts | 1 + .../src/providers/onelogin/provider.ts | 150 ++++++++++++++---- 2 files changed, 118 insertions(+), 33 deletions(-) diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index aba41bdb73..3f71dd2c26 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -25,6 +25,7 @@ export * from './microsoft'; export * from './oauth2'; export * from './oidc'; export * from './okta'; +export * from './onelogin'; export * from './saml'; export { factories as defaultAuthProviderFactories } from './factories'; diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index 66e8b0bfc5..97aec51a1e 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -36,7 +36,15 @@ import { executeFetchUserProfileStrategy, PassportDoneCallback, } from '../../lib/passport'; -import { RedirectInfo, AuthProviderFactory } from '../types'; +import { + RedirectInfo, + AuthProviderFactory, + AuthHandler, + SignInResolver, +} from '../types'; +import { CatalogIdentityClient } from '../../lib/catalog'; +import { Logger } from 'winston'; +import { TokenIssuer } from '../../identity'; type PrivateInfo = { refreshToken: string; @@ -44,12 +52,27 @@ type PrivateInfo = { export type Options = OAuthProviderOptions & { issuer: string; + signInResolver?: SignInResolver; + authHandler: AuthHandler; + tokenIssuer: TokenIssuer; + catalogIdentityClient: CatalogIdentityClient; + logger: Logger; }; export class OneLoginProvider implements OAuthHandlers { private readonly _strategy: any; + private readonly signInResolver?: SignInResolver; + private readonly authHandler: AuthHandler; + private readonly tokenIssuer: TokenIssuer; + private readonly catalogIdentityClient: CatalogIdentityClient; + private readonly logger: Logger; constructor(options: Options) { + this.signInResolver = options.signInResolver; + this.authHandler = options.authHandler; + this.tokenIssuer = options.tokenIssuer; + this.catalogIdentityClient = options.catalogIdentityClient; + this.logger = options.logger; this._strategy = new OneLoginStrategy( { issuer: options.issuer, @@ -97,18 +120,8 @@ export class OneLoginProvider implements OAuthHandlers { PrivateInfo >(req, this._strategy); - const profile = makeProfileInfo(result.fullProfile, result.params.id_token); - return { - response: await this.populateIdentity({ - profile, - providerInfo: { - idToken: result.params.id_token, - accessToken: result.accessToken, - scope: result.params.scope, - expiresInSeconds: result.params.expires_in, - }, - }), + response: await this.handleResult(result), refreshToken: privateInfo.refreshToken, }; } @@ -124,51 +137,122 @@ export class OneLoginProvider implements OAuthHandlers { this._strategy, accessToken, ); - const profile = makeProfileInfo(fullProfile, params.id_token); - return this.populateIdentity({ - providerInfo: { - accessToken, - idToken: params.id_token, - expiresInSeconds: params.expires_in, - scope: params.scope, - }, - profile, + return this.handleResult({ + fullProfile, + params, + accessToken, }); } - private async populateIdentity( - response: OAuthResponse, - ): Promise { - const { profile } = response; + private async handleResult(result: OAuthResult) { + const { profile } = await this.authHandler(result); - if (!profile.email) { - throw new Error('OIDC profile contained no email'); + const response: OAuthResponse = { + providerInfo: { + idToken: result.params.id_token, + accessToken: result.accessToken, + scope: result.params.scope, + expiresInSeconds: result.params.expires_in, + }, + profile, + }; + + if (this.signInResolver) { + response.backstageIdentity = await this.signInResolver( + { + result, + profile, + }, + { + tokenIssuer: this.tokenIssuer, + catalogIdentityClient: this.catalogIdentityClient, + logger: this.logger, + }, + ); } - const id = profile.email.split('@')[0]; - - return { ...response, backstageIdentity: { id, token: '' } }; + return response; } } -export type OneLoginProviderOptions = {}; +const defaultSignInResolver: SignInResolver = async ( + info, + ctx, +) => { + const { profile } = info; + + if (!profile.email) { + throw new Error('OIDC profile contained no email'); + } + + const id = profile.email.split('@')[0]; + + const token = await ctx.tokenIssuer.issueToken({ + claims: { sub: id, ent: [`user:default/${id}`] }, + }); + + return { id, token }; +}; + +export type OneLoginProviderOptions = { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; + + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; +}; export const createOneLoginProvider = ( - _options?: OneLoginProviderOptions, + options?: OneLoginProviderOptions, ): AuthProviderFactory => { - return ({ providerId, globalConfig, config, tokenIssuer }) => + return ({ + providerId, + globalConfig, + config, + tokenIssuer, + catalogApi, + logger, + }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); const issuer = envConfig.getString('issuer'); const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const catalogIdentityClient = new CatalogIdentityClient({ + catalogApi, + tokenIssuer, + }); + + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile, params.id_token), + }); + + const signInResolver = options?.signIn?.resolver ?? defaultSignInResolver; + const provider = new OneLoginProvider({ clientId, clientSecret, callbackUrl, issuer, + authHandler, + signInResolver, + tokenIssuer, + catalogIdentityClient, + logger, }); return OAuthAdapter.fromConfig(globalConfig, provider, { From 2f26120a36cb69adcfa8f19d3f1e9cc3b9b6cdc7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 28 Dec 2021 16:01:01 +0100 Subject: [PATCH 08/24] auth-backend: changeset and api report update for auth0 and onelogin Signed-off-by: Patrik Oldsberg --- .changeset/eighty-dancers-heal.md | 5 ++++ plugins/auth-backend/api-report.md | 26 +++++++++++++++++++ .../src/providers/auth0/provider.ts | 2 ++ .../src/providers/onelogin/provider.ts | 2 ++ 4 files changed, 35 insertions(+) create mode 100644 .changeset/eighty-dancers-heal.md diff --git a/.changeset/eighty-dancers-heal.md b/.changeset/eighty-dancers-heal.md new file mode 100644 index 0000000000..9286e75d2c --- /dev/null +++ b/.changeset/eighty-dancers-heal.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Update `auth0` and `onelogin` providers to allow for `authHandler` and `signIn.resolver` configuration. diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 9ed523c678..29de38a1f4 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -47,6 +47,14 @@ export type AtlassianProviderOptions = { }; }; +// @public (undocumented) +export type Auth0ProviderOptions = { + authHandler?: AuthHandler; + signIn?: { + resolver: SignInResolver; + }; +}; + // @public export type AuthHandler = ( input: AuthResult, @@ -219,6 +227,11 @@ export const createAtlassianProvider: ( options?: AtlassianProviderOptions | undefined, ) => AuthProviderFactory; +// @public (undocumented) +export const createAuth0Provider: ( + options?: Auth0ProviderOptions | undefined, +) => AuthProviderFactory; + // Warning: (ae-missing-release-tag) "createAwsAlbProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -282,6 +295,11 @@ export const createOktaProvider: ( _options?: OktaProviderOptions | undefined, ) => AuthProviderFactory; +// @public (undocumented) +export const createOneLoginProvider: ( + options?: OneLoginProviderOptions | undefined, +) => AuthProviderFactory; + // Warning: (ae-missing-release-tag) "createOriginFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -572,6 +590,14 @@ export type OktaProviderOptions = { }; }; +// @public (undocumented) +export type OneLoginProviderOptions = { + authHandler?: AuthHandler; + signIn?: { + resolver: SignInResolver; + }; +}; + // Warning: (ae-missing-release-tag) "postMessageResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index 4293e3e481..583f95c621 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -197,6 +197,7 @@ const defaultSignInResolver: SignInResolver = async ( return { id, token }; }; +/** @public */ export type Auth0ProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -215,6 +216,7 @@ export type Auth0ProviderOptions = { }; }; +/** @public */ export const createAuth0Provider = ( options?: Auth0ProviderOptions, ): AuthProviderFactory => { diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index 97aec51a1e..8cb06ea7a7 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -195,6 +195,7 @@ const defaultSignInResolver: SignInResolver = async ( return { id, token }; }; +/** @public */ export type OneLoginProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -213,6 +214,7 @@ export type OneLoginProviderOptions = { }; }; +/** @public */ export const createOneLoginProvider = ( options?: OneLoginProviderOptions, ): AuthProviderFactory => { From dd95ac91ccfc4d6437ec2850051f444df20826ed Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 28 Dec 2021 16:11:05 +0100 Subject: [PATCH 09/24] Refactor search plugin stories to use a shared mock search context provider. Signed-off-by: Eric Peterson --- .../DefaultResultListItem.stories.tsx | 71 ++++------ .../SearchBar/SearchBar.stories.tsx | 101 ++++--------- .../SearchContextForStorybook.stories.tsx | 42 ++++++ .../SearchFilter/SearchFilter.stories.tsx | 63 ++++----- .../SearchModal/SearchModal.stories.tsx | 74 +++++----- .../SearchResult/SearchResult.stories.tsx | 133 +++++++++--------- .../SearchType/SearchType.stories.tsx | 45 +++--- 7 files changed, 253 insertions(+), 276 deletions(-) create mode 100644 plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx diff --git a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx index 0445a35943..fe2f6c2327 100644 --- a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx +++ b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx @@ -25,6 +25,17 @@ import { MemoryRouter } from 'react-router'; export default { title: 'Plugins/Search/DefaultResultListItem', component: DefaultResultListItem, + decorators: [ + (Story: () => JSX.Element) => ( + + + + + + + + ), + ], }; const mockSearchResult = { @@ -35,54 +46,34 @@ const mockSearchResult = { }; export const Default = () => { - return ( - - - - - - - - ); + return ; }; export const WithIcon = () => { return ( - - - - } - /> - - - + } + /> ); }; export const WithSecondaryAction = () => { return ( - - - - } - style={{ textTransform: 'lowercase' }} - > - {mockSearchResult.owner} - - } - /> - - - + } + style={{ textTransform: 'lowercase' }} + > + {mockSearchResult.owner} + + } + /> ); }; diff --git a/plugins/search/src/components/SearchBar/SearchBar.stories.tsx b/plugins/search/src/components/SearchBar/SearchBar.stories.tsx index d00f391bc7..96c86aa16f 100644 --- a/plugins/search/src/components/SearchBar/SearchBar.stories.tsx +++ b/plugins/search/src/components/SearchBar/SearchBar.stories.tsx @@ -14,88 +14,58 @@ * limitations under the License. */ -import React from 'react'; +import React, { ComponentType } from 'react'; import { Paper, Grid, makeStyles } from '@material-ui/core'; -import { SearchBar, SearchContext } from '../index'; -import { MemoryRouter } from 'react-router'; +import { SearchBar } from '../index'; +import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories'; export default { title: 'Plugins/Search/SearchBar', component: SearchBar, -}; - -const defaultValue = { - term: '', - setTerm: () => {}, + decorators: [ + (Story: ComponentType<{}>) => ( + + + + + + + + ), + ], }; export const Default = () => { return ( - - {/* @ts-ignore (defaultValue requires more than what is used here) */} - - - - - - - - - - + + + ); }; export const CustomPlaceholder = () => { return ( - - {/* @ts-ignore (defaultValue requires more than what is used here) */} - - - - - - - - - - + + + ); }; export const Focused = () => { return ( - - {/* @ts-ignore (defaultValue requires more than what is used here) */} - - - - - {/* decision up to adopter, read https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-autofocus.md#no-autofocus */} - {/* eslint-disable-next-line jsx-a11y/no-autofocus */} - - - - - - + + {/* decision up to adopter, read https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-autofocus.md#no-autofocus */} + {/* eslint-disable-next-line jsx-a11y/no-autofocus */} + + ); }; export const WithoutClearButton = () => { return ( - - {/* @ts-ignore (defaultValue requires more than what is used here) */} - - - - - - - - - - + + + ); }; @@ -112,17 +82,8 @@ const useStyles = makeStyles({ export const CustomStyles = () => { const classes = useStyles(); return ( - - {/* @ts-ignore (defaultValue requires more than what is used here) */} - - - - - - - - - - + + + ); }; diff --git a/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx b/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx new file mode 100644 index 0000000000..4528a6e46d --- /dev/null +++ b/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ApiProvider } from '@backstage/core-app-api'; +import { SearchResultSet } from '@backstage/search-common'; +import { TestApiRegistry } from '@backstage/test-utils'; +import React, { ComponentProps } from 'react'; +import { searchApiRef } from '../../apis'; +import { SearchContextProvider as RealSearchContextProvider } from './SearchContext'; + +type QueryResultProps = { + mockedResults?: SearchResultSet; +}; + +/** + * Utility context provider only for use in Storybook stories. + */ +export const SearchContextProvider = ( + props: ComponentProps & QueryResultProps, +) => { + const { mockedResults, ...contextProps } = props; + const query: any = () => Promise.resolve(mockedResults || {}); + const apiRegistry = TestApiRegistry.from([searchApiRef, { query }]); + + return ( + + + + ); +}; diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx b/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx index 190856fca8..806ee46e60 100644 --- a/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx +++ b/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx @@ -14,56 +14,45 @@ * limitations under the License. */ -import React from 'react'; +import React, { ComponentType } from 'react'; import { Grid, Paper } from '@material-ui/core'; -import { SearchFilter, SearchContext } from '../index'; -import { MemoryRouter } from 'react-router'; +import { SearchFilter } from '../index'; +import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories'; export default { title: 'Plugins/Search/SearchFilter', component: SearchFilter, -}; - -const defaultValue = { - filters: {}, + decorators: [ + (Story: ComponentType<{}>) => ( + + + + + + + + ), + ], }; export const CheckBoxFilter = () => { return ( - - {/* @ts-ignore (defaultValue requires more than what is used here) */} - - - - - - - - - - + + + ); }; export const SelectFilter = () => { return ( - - {/* @ts-ignore (defaultValue requires more than what is used here) */} - - - - - - - - - - + + + ); }; diff --git a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx index 6fa93c6588..d46e44c580 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx @@ -16,60 +16,50 @@ import React, { ComponentType } from 'react'; import { Button } from '@material-ui/core'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { wrapInTestApp } from '@backstage/test-utils'; import { SearchModal } from '../index'; -import { useSearch, SearchContextProvider } from '../SearchContext'; -import { searchApiRef } from '../../apis'; +import { useSearch } from '../SearchContext'; import { rootRouteRef } from '../../plugin'; +import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories'; -const mockSearchApi = { - query: () => - Promise.resolve({ - results: [ - { - type: 'custom-result-item', - document: { - location: 'search/search-result-1', - title: 'Search Result 1', - text: 'some text from the search result', - }, - }, - { - type: 'no-custom-result-item', - document: { - location: 'search/search-result-2', - title: 'Search Result 2', - text: 'some text from the search result', - }, - }, - { - type: 'no-custom-result-item', - document: { - location: 'search/search-result-3', - title: 'Search Result 3', - text: 'some text from the search result', - }, - }, - ], - }), +const mockResults = { + results: [ + { + type: 'custom-result-item', + document: { + location: 'search/search-result-1', + title: 'Search Result 1', + text: 'some text from the search result', + }, + }, + { + type: 'no-custom-result-item', + document: { + location: 'search/search-result-2', + title: 'Search Result 2', + text: 'some text from the search result', + }, + }, + { + type: 'no-custom-result-item', + document: { + location: 'search/search-result-3', + title: 'Search Result 3', + text: 'some text from the search result', + }, + }, + ], }; -const apiRegistry = () => ApiRegistry.from([[searchApiRef, mockSearchApi]]); - export default { title: 'Plugins/Search/SearchModal', component: SearchModal, decorators: [ (Story: ComponentType<{}>) => wrapInTestApp( - <> - - - - - - , + + + , { mountedRoutes: { '/search': rootRouteRef } }, ), ], diff --git a/plugins/search/src/components/SearchResult/SearchResult.stories.tsx b/plugins/search/src/components/SearchResult/SearchResult.stories.tsx index aebaecb8b4..bdd04ad90f 100644 --- a/plugins/search/src/components/SearchResult/SearchResult.stories.tsx +++ b/plugins/search/src/components/SearchResult/SearchResult.stories.tsx @@ -14,83 +14,82 @@ * limitations under the License. */ -import React from 'react'; +import React, { ComponentType } from 'react'; import { List, ListItem } from '@material-ui/core'; -import { SearchResult, SearchContext, DefaultResultListItem } from '../index'; +import { SearchResult, DefaultResultListItem } from '../index'; import { MemoryRouter } from 'react-router'; import { Link } from '@backstage/core-components'; +import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories'; + +const mockResults = { + results: [ + { + type: 'custom-result-item', + document: { + location: 'search/search-result-1', + title: 'Search Result 1', + text: 'some text from the search result', + }, + }, + { + type: 'no-custom-result-item', + document: { + location: 'search/search-result-2', + title: 'Search Result 2', + text: 'some text from the search result', + }, + }, + { + type: 'no-custom-result-item', + document: { + location: 'search/search-result-3', + title: 'Search Result 3', + text: 'some text from the search result', + }, + }, + ], +}; export default { title: 'Plugins/Search/SearchResult', component: SearchResult, -}; - -const defaultValue = { - result: { - loading: false, - error: '', - value: { - results: [ - { - type: 'custom-result-item', - document: { - location: 'search/search-result-1', - title: 'Search Result 1', - text: 'some text from the search result', - }, - }, - { - type: 'no-custom-result-item', - document: { - location: 'search/search-result-2', - title: 'Search Result 2', - text: 'some text from the search result', - }, - }, - { - type: 'no-custom-result-item', - document: { - location: 'search/search-result-3', - title: 'Search Result 3', - text: 'some text from the search result', - }, - }, - ], - }, - }, + decorators: [ + (Story: ComponentType<{}>) => ( + + + + + + ), + ], }; export const Default = () => { return ( - - {/* @ts-ignore (defaultValue requires more than what is used here) */} - - - {({ results }) => ( - - {results.map(({ type, document }) => { - switch (type) { - case 'custom-result-item': - return ( - - ); - default: - return ( - - - {document.title} - {document.text} - - - ); - } - })} - - )} - - - + + {({ results }) => ( + + {results.map(({ type, document }) => { + switch (type) { + case 'custom-result-item': + return ( + + ); + default: + return ( + + + {document.title} - {document.text} + + + ); + } + })} + + )} + ); }; diff --git a/plugins/search/src/components/SearchType/SearchType.stories.tsx b/plugins/search/src/components/SearchType/SearchType.stories.tsx index d458c59aad..b8282d6423 100644 --- a/plugins/search/src/components/SearchType/SearchType.stories.tsx +++ b/plugins/search/src/components/SearchType/SearchType.stories.tsx @@ -13,46 +13,51 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useState } from 'react'; +import React, { ComponentType } from 'react'; +import { Grid, Paper } from '@material-ui/core'; import CatalogIcon from '@material-ui/icons/MenuBook'; import DocsIcon from '@material-ui/icons/Description'; import UsersGroupsIcon from '@material-ui/icons/Person'; import { SearchType } from '../index'; -import { SearchContext } from '../SearchContext'; +import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories'; export default { title: 'Plugins/Search/SearchType', component: SearchType, + decorators: [ + (Story: ComponentType<{}>) => ( + + + + + + + + ), + ], }; const values = ['value-1', 'value-2', 'value-3']; export const Default = () => { - const [types, setTypes] = useState([]); - return ( - + - + ); }; export const Accordion = () => { - const [types, setTypes] = useState([]); - const setPageCursor = () => {}; - return ( - - }, - { value: 'value-2', name: 'Value Two', icon: }, - { value: 'value-3', name: 'Value Three', icon: }, - ]} - /> - + }, + { value: 'value-2', name: 'Value Two', icon: }, + { value: 'value-3', name: 'Value Three', icon: }, + ]} + /> ); }; From eeb2269bb92563c122b5c0e95113dd38806aa47d Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 28 Dec 2021 17:08:01 +0100 Subject: [PATCH 10/24] Add basic tests for Signed-off-by: Eric Peterson --- .../SearchType/SearchType.Accordion.test.tsx | 119 ++++++++++++++++++ .../SearchType/SearchType.Accordion.tsx | 2 +- 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx diff --git a/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx new file mode 100644 index 0000000000..d56b40b879 --- /dev/null +++ b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx @@ -0,0 +1,119 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import user from '@testing-library/user-event'; + +import { SearchContext } from '../SearchContext'; +import { SearchType } from './SearchType'; + +describe('SearchType.Accordion', () => { + const contextSpy = { + result: { loading: false, value: { results: [] } }, + term: '', + types: [], + filters: {}, + toggleModal: jest.fn(), + setTerm: jest.fn(), + setTypes: jest.fn(), + setFilters: jest.fn(), + setPageCursor: jest.fn(), + }; + + const expectedLabel = 'Expected Label'; + const expectedType = { + value: 'expected-type', + name: 'Expected Type', + icon: <>, + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should render as expected', () => { + const { getByText } = render( + + + , + ); + + // The given label should be rendered. + expect(getByText(expectedLabel)).toBeInTheDocument(); + + // "Collapse" is visible by default (element is not collapsed) + expect(getByText('Collapse')).toBeInTheDocument(); + + // The default "all" type should be rendered. + expect(getByText('All')).toBeInTheDocument(); + + // The given type is also visible + expect(getByText(expectedType.name)).toBeInTheDocument(); + }); + + it('should set entire types array when a type is selected', () => { + const { getByText } = render( + + + , + ); + + user.click(getByText(expectedType.name)); + + expect(contextSpy.setTypes).toHaveBeenCalledWith([expectedType.value]); + }); + + it('should reset types array when all is selected', () => { + const { getByText } = render( + + + , + ); + + user.click(getByText('All')); + + expect(contextSpy.setTypes).toHaveBeenCalledWith([]); + }); + + it('should reset page cursor when a new type is selected', () => { + const { getByText } = render( + + + , + ); + + user.click(getByText(expectedType.name)); + + expect(contextSpy.setPageCursor).toHaveBeenCalledWith(undefined); + }); + + it('should collapse when a new type is selected', () => { + const { getByText, queryByText } = render( + + + , + ); + + user.click(getByText(expectedType.name)); + + expect(queryByText('Collapse')).not.toBeInTheDocument(); + }); +}); diff --git a/plugins/search/src/components/SearchType/SearchType.Accordion.tsx b/plugins/search/src/components/SearchType/SearchType.Accordion.tsx index 9ad03db8a1..57e6609bab 100644 --- a/plugins/search/src/components/SearchType/SearchType.Accordion.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Accordion.tsx @@ -93,7 +93,7 @@ export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => { const handleClick = (type: string) => { return () => { setTypes(type !== '' ? [type] : []); - setPageCursor(''); + setPageCursor(undefined); setExpanded(false); }; }; From 62f36517072ebe147fac6b8894a14135e5662e37 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Tue, 28 Dec 2021 09:19:09 -0700 Subject: [PATCH 11/24] Add warning to default backend Signed-off-by: Tim Hansen --- .../templates/default-app/packages/backend/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/create-app/templates/default-app/packages/backend/src/index.ts b/packages/create-app/templates/default-app/packages/backend/src/index.ts index fbeabcff36..08d21e61f7 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/index.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/index.ts @@ -81,6 +81,8 @@ async function main() { apiRouter.use('/techdocs', await techdocs(techdocsEnv)); apiRouter.use('/proxy', await proxy(proxyEnv)); apiRouter.use('/search', await search(searchEnv)); + + // Add backends ABOVE this line; this 404 handler is the catch-all fallback apiRouter.use(notFoundHandler()); const service = createServiceBuilder(module) From c88cdacc1abce1f77b93a3cf1757211cfba64e79 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 28 Dec 2021 17:30:00 +0100 Subject: [PATCH 12/24] auth-backend: always exchange and never return refresh tokens to clients Signed-off-by: Patrik Oldsberg --- .changeset/four-phones-shave.md | 29 ++++++++++ plugins/auth-backend/api-report.md | 25 +++++---- .../src/lib/oauth/OAuthAdapter.test.ts | 30 +++++++---- .../src/lib/oauth/OAuthAdapter.ts | 12 ++--- plugins/auth-backend/src/lib/oauth/types.ts | 9 ++-- .../src/providers/atlassian/provider.test.ts | 54 ++++++++++--------- .../src/providers/atlassian/provider.ts | 38 ++++++------- .../src/providers/auth0/provider.ts | 31 +++++------ .../src/providers/bitbucket/provider.ts | 31 +++++------ .../src/providers/github/provider.test.ts | 34 ++++++------ .../src/providers/github/provider.ts | 33 ++++++------ .../src/providers/gitlab/provider.test.ts | 32 +++++------ .../src/providers/gitlab/provider.ts | 37 ++++++------- .../src/providers/google/provider.ts | 32 +++++------ .../src/providers/microsoft/provider.ts | 31 +++++------ .../src/providers/oauth2/provider.ts | 27 ++++------ .../src/providers/oidc/provider.ts | 24 ++++----- .../src/providers/okta/provider.ts | 19 ++++--- .../src/providers/onelogin/provider.ts | 30 ++++++----- 19 files changed, 296 insertions(+), 262 deletions(-) create mode 100644 .changeset/four-phones-shave.md diff --git a/.changeset/four-phones-shave.md b/.changeset/four-phones-shave.md new file mode 100644 index 0000000000..1ef58bdadb --- /dev/null +++ b/.changeset/four-phones-shave.md @@ -0,0 +1,29 @@ +--- +'@backstage/plugin-auth-backend': minor +--- + +Avoid ever returning OAuth refresh tokens back to the client, and always exchange refresh tokens for a new one when available for all providers. + +This comes with a breaking change to the TypeScript API for custom auth providers. The `refresh` method of `OAuthHandlers` implementation must now return a `{ response, refreshToken }` object rather than a direct response. Existing `refresh` implementations are typically migrated by changing an existing return expression that looks like this: + +```ts +return await this.handleResult({ + fullProfile, + params, + accessToken, + refreshToken, +}); +``` + +Into the following: + +```ts +return { + response: await this.handleResult({ + fullProfile, + params, + accessToken, + }), + refreshToken, +}; +``` diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 29de38a1f4..af46d874df 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -27,10 +27,13 @@ export class AtlassianAuthProvider implements OAuthHandlers { // (undocumented) handler(req: express.Request): Promise<{ response: OAuthResponse; - refreshToken: string; + refreshToken: string | undefined; }>; // (undocumented) - refresh(req: OAuthRefreshRequest): Promise; + refresh(req: OAuthRefreshRequest): Promise<{ + response: OAuthResponse; + refreshToken: string | undefined; + }>; // Warning: (ae-forgotten-export) The symbol "RedirectInfo" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -488,7 +491,10 @@ export interface OAuthHandlers { // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' - refresh?(req: OAuthRefreshRequest): Promise; + refresh?(req: OAuthRefreshRequest): Promise<{ + response: OAuthResponse; + refreshToken?: string; + }>; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen @@ -503,7 +509,6 @@ export type OAuthProviderInfo = { idToken?: string; expiresInSeconds?: number; scope: string; - refreshToken?: string; }; // Warning: (ae-missing-release-tag) "OAuthProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -703,11 +708,11 @@ export type WebMessageResponse = // // src/identity/types.d.ts:31:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts // src/providers/aws-alb/provider.d.ts:77:5 - (ae-forgotten-export) The symbol "AwsAlbResult" needs to be exported by the entry point index.d.ts -// src/providers/github/provider.d.ts:71:58 - (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag -// src/providers/github/provider.d.ts:71:90 - (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag -// src/providers/github/provider.d.ts:71:89 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/providers/github/provider.d.ts:71:67 - (tsdoc-malformed-html-name) Invalid HTML element: Expecting an HTML name -// src/providers/github/provider.d.ts:71:68 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/providers/github/provider.d.ts:78:5 - (ae-forgotten-export) The symbol "StateEncoder" needs to be exported by the entry point index.d.ts +// src/providers/github/provider.d.ts:74:58 - (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag +// src/providers/github/provider.d.ts:74:90 - (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag +// src/providers/github/provider.d.ts:74:89 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/providers/github/provider.d.ts:74:67 - (tsdoc-malformed-html-name) Invalid HTML element: Expecting an HTML name +// src/providers/github/provider.d.ts:74:68 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/providers/github/provider.d.ts:81:5 - (ae-forgotten-export) The symbol "StateEncoder" needs to be exported by the entry point index.d.ts // src/providers/types.d.ts:100:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts ``` diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts index a3fc77bc02..98f4cd9fa1 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts @@ -57,7 +57,10 @@ describe('OAuthAdapter', () => { }; } async refresh() { - return mockResponseData; + return { + response: mockResponseData, + refreshToken: 'token', + }; } } const providerInstance = new MyAuthProvider(); @@ -257,7 +260,10 @@ describe('OAuthAdapter', () => { }); it('correctly populates incomplete identities', async () => { - const mockRefresh = jest.fn, [express.Request]>(); + const mockRefresh = jest.fn< + Promise<{ response: OAuthResponse }>, + [express.Request] + >(); const oauthProvider = new OAuthAdapter( { @@ -291,10 +297,12 @@ describe('OAuthAdapter', () => { // Without a token mockRefresh.mockResolvedValueOnce({ - ...mockResponseData, - backstageIdentity: { - id: 'foo', - token: '', + response: { + ...mockResponseData, + backstageIdentity: { + id: 'foo', + token: '', + }, }, }); await oauthProvider.refresh(mockRequest, mockResponse); @@ -315,10 +323,12 @@ describe('OAuthAdapter', () => { // With a token mockRefresh.mockResolvedValueOnce({ - ...mockResponseData, - backstageIdentity: { - id: 'foo', - token: `z.${mkTokenBody({ sub: 'user:my-ns/foo' })}.z`, + response: { + ...mockResponseData, + backstageIdentity: { + id: 'foo', + token: `z.${mkTokenBody({ sub: 'user:my-ns/foo' })}.z`, + }, }, }); await oauthProvider.refresh(mockRequest, mockResponse); diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index 4d5d507aa1..ce4b52ef4f 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -212,19 +212,15 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { const forwardReq = Object.assign(req, { scope, refreshToken }); // get new access_token - const response = await this.handlers.refresh( - forwardReq as OAuthRefreshRequest, - ); + const { response, refreshToken: newRefreshToken } = + await this.handlers.refresh(forwardReq as OAuthRefreshRequest); const backstageIdentity = await this.populateIdentity( response.backstageIdentity, ); - if ( - response.providerInfo.refreshToken && - response.providerInfo.refreshToken !== refreshToken - ) { - this.setRefreshTokenCookie(res, response.providerInfo.refreshToken); + if (newRefreshToken && newRefreshToken !== refreshToken) { + this.setRefreshTokenCookie(res, newRefreshToken); } res.status(200).json({ ...response, backstageIdentity }); diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index cd1439b399..f54d04d15b 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -79,10 +79,6 @@ export type OAuthProviderInfo = { * Scopes granted for the access token. */ scope: string; - /** - * A refresh token issued for the signed in user - */ - refreshToken?: string; }; export type OAuthState = { @@ -130,7 +126,10 @@ export interface OAuthHandlers { * @param {string} refreshToken * @param {string} scope */ - refresh?(req: OAuthRefreshRequest): Promise; + refresh?(req: OAuthRefreshRequest): Promise<{ + response: OAuthResponse; + refreshToken?: string; + }>; /** * (Optional) Sign out of the auth provider. diff --git a/plugins/auth-backend/src/providers/atlassian/provider.test.ts b/plugins/auth-backend/src/providers/atlassian/provider.test.ts index 7bed582f81..29241dd17b 100644 --- a/plugins/auth-backend/src/providers/atlassian/provider.test.ts +++ b/plugins/auth-backend/src/providers/atlassian/provider.test.ts @@ -78,20 +78,22 @@ describe('createAtlassianProvider', () => { refreshToken: 'wacka', }, }); - const { response } = await provider.handler({} as any); - expect(response).toEqual({ - providerInfo: { - accessToken: 'accessToken', - expiresInSeconds: 123, - idToken: 'idToken', - scope: 'scope', - refreshToken: 'wacka', - }, - profile: { - email: 'conrad@example.com', - displayName: 'Conrad', - picture: 'http://google.com/lols', + const result = await provider.handler({} as any); + expect(result).toEqual({ + response: { + providerInfo: { + accessToken: 'accessToken', + expiresInSeconds: 123, + idToken: 'idToken', + scope: 'scope', + }, + profile: { + email: 'conrad@example.com', + displayName: 'Conrad', + picture: 'http://google.com/lols', + }, }, + refreshToken: 'wacka', }); }); @@ -127,20 +129,22 @@ describe('createAtlassianProvider', () => { ], }); - const response = await provider.refresh({} as any); + const result = await provider.refresh({} as any); - expect(response).toEqual({ - profile: { - displayName: 'Mocked User', - email: 'mockuser@gmail.com', - picture: 'http://google.com/lols', - }, - providerInfo: { - accessToken: 'a.b.c', - idToken: 'my-id', - refreshToken: 'dont-forget-to-send-refresh', - scope: 'read_user', + expect(result).toEqual({ + response: { + profile: { + displayName: 'Mocked User', + email: 'mockuser@gmail.com', + picture: 'http://google.com/lols', + }, + providerInfo: { + accessToken: 'a.b.c', + idToken: 'my-id', + scope: 'read_user', + }, }, + refreshToken: 'dont-forget-to-send-refresh', }); }); }); diff --git a/plugins/auth-backend/src/providers/atlassian/provider.ts b/plugins/auth-backend/src/providers/atlassian/provider.ts index e19f29a3a4..ba402eace0 100644 --- a/plugins/auth-backend/src/providers/atlassian/provider.ts +++ b/plugins/auth-backend/src/providers/atlassian/provider.ts @@ -107,9 +107,7 @@ export class AtlassianAuthProvider implements OAuthHandlers { }); } - async handler( - req: express.Request, - ): Promise<{ response: OAuthResponse; refreshToken: string }> { + async handler(req: express.Request) { const { result } = await executeFrameHandlerStrategy( req, this._strategy, @@ -117,7 +115,7 @@ export class AtlassianAuthProvider implements OAuthHandlers { return { response: await this.handleResult(result), - refreshToken: result.refreshToken ?? '', + refreshToken: result.refreshToken, }; } @@ -128,7 +126,6 @@ export class AtlassianAuthProvider implements OAuthHandlers { providerInfo: { idToken: result.params.id_token, accessToken: result.accessToken, - refreshToken: result.refreshToken, scope: result.params.scope, expiresInSeconds: result.params.expires_in, }, @@ -152,28 +149,27 @@ export class AtlassianAuthProvider implements OAuthHandlers { return response; } - async refresh(req: OAuthRefreshRequest): Promise { - const { - accessToken, - params, - refreshToken: newRefreshToken, - } = await executeRefreshTokenStrategy( - this._strategy, - req.refreshToken, - req.scope, - ); + async refresh(req: OAuthRefreshRequest) { + const { accessToken, params, refreshToken } = + await executeRefreshTokenStrategy( + this._strategy, + req.refreshToken, + req.scope, + ); const fullProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, ); - return this.handleResult({ - fullProfile, - params, - accessToken, - refreshToken: newRefreshToken, - }); + return { + response: await this.handleResult({ + fullProfile, + params, + accessToken, + }), + refreshToken, + }; } } diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index 583f95c621..4677e07b4d 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -113,9 +113,7 @@ export class Auth0AuthProvider implements OAuthHandlers { }); } - async handler( - req: express.Request, - ): Promise<{ response: OAuthResponse; refreshToken: string }> { + async handler(req: express.Request) { const { result, privateInfo } = await executeFrameHandlerStrategy< OAuthResult, PrivateInfo @@ -127,24 +125,27 @@ export class Auth0AuthProvider implements OAuthHandlers { }; } - async refresh(req: OAuthRefreshRequest): Promise { - const { accessToken, params } = await executeRefreshTokenStrategy( - this._strategy, - req.refreshToken, - req.scope, - ); + async refresh(req: OAuthRefreshRequest) { + const { accessToken, refreshToken, params } = + await executeRefreshTokenStrategy( + this._strategy, + req.refreshToken, + req.scope, + ); const fullProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, ); - return this.handleResult({ - fullProfile, - params, - accessToken, - refreshToken: req.refreshToken, - }); + return { + response: await this.handleResult({ + fullProfile, + params, + accessToken, + }), + refreshToken, + }; } private async handleResult(result: OAuthResult) { diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.ts b/plugins/auth-backend/src/providers/bitbucket/provider.ts index 3a518a6baa..5ad9a739d9 100644 --- a/plugins/auth-backend/src/providers/bitbucket/provider.ts +++ b/plugins/auth-backend/src/providers/bitbucket/provider.ts @@ -138,9 +138,7 @@ export class BitbucketAuthProvider implements OAuthHandlers { }); } - async handler( - req: express.Request, - ): Promise<{ response: OAuthResponse; refreshToken: string }> { + async handler(req: express.Request) { const { result, privateInfo } = await executeFrameHandlerStrategy< OAuthResult, PrivateInfo @@ -152,22 +150,25 @@ export class BitbucketAuthProvider implements OAuthHandlers { }; } - async refresh(req: OAuthRefreshRequest): Promise { - const { accessToken, params } = await executeRefreshTokenStrategy( - this._strategy, - req.refreshToken, - req.scope, - ); + async refresh(req: OAuthRefreshRequest) { + const { accessToken, refreshToken, params } = + await executeRefreshTokenStrategy( + this._strategy, + req.refreshToken, + req.scope, + ); const fullProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, ); - return this.handleResult({ - fullProfile, - params, - accessToken, - refreshToken: req.refreshToken, - }); + return { + response: await this.handleResult({ + fullProfile, + params, + accessToken, + }), + refreshToken, + }; } private async handleResult(result: BitbucketOAuthResult) { diff --git a/plugins/auth-backend/src/providers/github/provider.test.ts b/plugins/auth-backend/src/providers/github/provider.test.ts index e418ab22c2..b11ac7f5a1 100644 --- a/plugins/auth-backend/src/providers/github/provider.test.ts +++ b/plugins/auth-backend/src/providers/github/provider.test.ts @@ -316,24 +316,26 @@ describe('GithubAuthProvider', () => { ], }); - const response = await provider.refresh({} as any); + const result = await provider.refresh({} as any); - expect(response).toEqual({ - backstageIdentity: { - id: 'mockuser', - token: 'token-for-mockuser', - }, - profile: { - displayName: 'Mocked User', - email: 'mockuser@gmail.com', - picture: undefined, - }, - providerInfo: { - accessToken: 'a.b.c', - refreshToken: 'dont-forget-to-send-refresh', - expiresInSeconds: 123, - scope: 'read_user', + expect(result).toEqual({ + response: { + backstageIdentity: { + id: 'mockuser', + token: 'token-for-mockuser', + }, + profile: { + displayName: 'Mocked User', + email: 'mockuser@gmail.com', + picture: undefined, + }, + providerInfo: { + accessToken: 'a.b.c', + expiresInSeconds: 123, + scope: 'read_user', + }, }, + refreshToken: 'dont-forget-to-send-refresh', }); }); }); diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index c7e82cc5ff..b832f4f77b 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -129,26 +129,26 @@ export class GithubAuthProvider implements OAuthHandlers { }; } - async refresh(req: OAuthRefreshRequest): Promise { - const { - accessToken, - refreshToken: newRefreshToken, - params, - } = await executeRefreshTokenStrategy( - this._strategy, - req.refreshToken, - req.scope, - ); + async refresh(req: OAuthRefreshRequest) { + const { accessToken, refreshToken, params } = + await executeRefreshTokenStrategy( + this._strategy, + req.refreshToken, + req.scope, + ); const fullProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, ); - return this.handleResult({ - fullProfile, - params, - accessToken, - refreshToken: newRefreshToken, - }); + + return { + response: await this.handleResult({ + fullProfile, + params, + accessToken, + }), + refreshToken, + }; } private async handleResult(result: GithubOAuthResult) { @@ -158,7 +158,6 @@ export class GithubAuthProvider implements OAuthHandlers { const response: OAuthResponse = { providerInfo: { accessToken: result.accessToken, - refreshToken: result.refreshToken, // GitHub expires the old refresh token when used scope: result.params.scope, expiresInSeconds: expiresInStr === undefined ? undefined : Number(expiresInStr), diff --git a/plugins/auth-backend/src/providers/gitlab/provider.test.ts b/plugins/auth-backend/src/providers/gitlab/provider.test.ts index d1a84b43e8..f90de3c75b 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.test.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.test.ts @@ -184,23 +184,25 @@ describe('GitlabAuthProvider', () => { ], }); - const response = await provider.refresh({} as any); + const result = await provider.refresh({} as any); - expect(response).toEqual({ - backstageIdentity: { - id: 'mockuser', - }, - profile: { - displayName: 'Mocked User', - email: 'mockuser@gmail.com', - picture: 'http://gitlab.com/lols', - }, - providerInfo: { - accessToken: 'a.b.c', - idToken: 'my-id', - refreshToken: 'dont-forget-to-send-refresh', - scope: 'read_user', + expect(result).toEqual({ + response: { + backstageIdentity: { + id: 'mockuser', + }, + profile: { + displayName: 'Mocked User', + email: 'mockuser@gmail.com', + picture: 'http://gitlab.com/lols', + }, + providerInfo: { + accessToken: 'a.b.c', + idToken: 'my-id', + scope: 'read_user', + }, }, + refreshToken: 'dont-forget-to-send-refresh', }); }); }); diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 6ca405d15f..dedbb7bd5b 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -132,9 +132,7 @@ export class GitlabAuthProvider implements OAuthHandlers { }); } - async handler( - req: express.Request, - ): Promise<{ response: OAuthResponse; refreshToken: string }> { + async handler(req: express.Request) { const { result, privateInfo } = await executeFrameHandlerStrategy< OAuthResult, PrivateInfo @@ -146,28 +144,26 @@ export class GitlabAuthProvider implements OAuthHandlers { }; } - async refresh(req: OAuthRefreshRequest): Promise { - const { - accessToken, - refreshToken: newRefreshToken, - params, - } = await executeRefreshTokenStrategy( - this._strategy, - req.refreshToken, - req.scope, - ); + async refresh(req: OAuthRefreshRequest) { + const { accessToken, refreshToken, params } = + await executeRefreshTokenStrategy( + this._strategy, + req.refreshToken, + req.scope, + ); const fullProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, ); - - return this.handleResult({ - fullProfile, - params, - accessToken, - refreshToken: newRefreshToken, - }); + return { + response: await this.handleResult({ + fullProfile, + params, + accessToken, + }), + refreshToken, + }; } private async handleResult(result: OAuthResult): Promise { @@ -177,7 +173,6 @@ export class GitlabAuthProvider implements OAuthHandlers { providerInfo: { idToken: result.params.id_token, accessToken: result.accessToken, - refreshToken: result.refreshToken, // GitLab expires the old refresh token when used scope: result.params.scope, expiresInSeconds: result.params.expires_in, }, diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index d85fc2de2d..10123bfe6f 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -113,9 +113,7 @@ export class GoogleAuthProvider implements OAuthHandlers { }); } - async handler( - req: express.Request, - ): Promise<{ response: OAuthResponse; refreshToken: string }> { + async handler(req: express.Request) { const { result, privateInfo } = await executeFrameHandlerStrategy< OAuthResult, PrivateInfo @@ -127,22 +125,26 @@ export class GoogleAuthProvider implements OAuthHandlers { }; } - async refresh(req: OAuthRefreshRequest): Promise { - const { accessToken, params } = await executeRefreshTokenStrategy( - this._strategy, - req.refreshToken, - req.scope, - ); + async refresh(req: OAuthRefreshRequest) { + const { accessToken, refreshToken, params } = + await executeRefreshTokenStrategy( + this._strategy, + req.refreshToken, + req.scope, + ); const fullProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, ); - return this.handleResult({ - fullProfile, - params, - accessToken, - refreshToken: req.refreshToken, - }); + + return { + response: await this.handleResult({ + fullProfile, + params, + accessToken, + }), + refreshToken, + }; } private async handleResult(result: OAuthResult) { diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index b21218c066..7e928a2786 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -104,9 +104,7 @@ export class MicrosoftAuthProvider implements OAuthHandlers { }); } - async handler( - req: express.Request, - ): Promise<{ response: OAuthResponse; refreshToken: string }> { + async handler(req: express.Request) { const { result, privateInfo } = await executeFrameHandlerStrategy< OAuthResult, PrivateInfo @@ -118,24 +116,27 @@ export class MicrosoftAuthProvider implements OAuthHandlers { }; } - async refresh(req: OAuthRefreshRequest): Promise { - const { accessToken, params } = await executeRefreshTokenStrategy( - this._strategy, - req.refreshToken, - req.scope, - ); + async refresh(req: OAuthRefreshRequest) { + const { accessToken, refreshToken, params } = + await executeRefreshTokenStrategy( + this._strategy, + req.refreshToken, + req.scope, + ); const fullProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, ); - return this.handleResult({ - fullProfile, - params, - accessToken, - refreshToken: req.refreshToken, - }); + return { + response: await this.handleResult({ + fullProfile, + params, + accessToken, + }), + refreshToken, + }; } private async handleResult(result: OAuthResult) { diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index 2f9c739860..dc4afbac48 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -127,9 +127,7 @@ export class OAuth2AuthProvider implements OAuthHandlers { }); } - async handler( - req: express.Request, - ): Promise<{ response: OAuthResponse; refreshToken: string }> { + async handler(req: express.Request) { const { result, privateInfo } = await executeFrameHandlerStrategy< OAuthResult, PrivateInfo @@ -141,29 +139,27 @@ export class OAuth2AuthProvider implements OAuthHandlers { }; } - async refresh(req: OAuthRefreshRequest): Promise { + async refresh(req: OAuthRefreshRequest) { const refreshTokenResponse = await executeRefreshTokenStrategy( this._strategy, req.refreshToken, req.scope, ); - const { - accessToken, - params, - refreshToken: updatedRefreshToken, - } = refreshTokenResponse; + const { accessToken, params, refreshToken } = refreshTokenResponse; const fullProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, ); - return this.handleResult({ - fullProfile, - params, - accessToken, - refreshToken: updatedRefreshToken, - }); + return { + response: await this.handleResult({ + fullProfile, + params, + accessToken, + }), + refreshToken, + }; } private async handleResult(result: OAuthResult) { @@ -175,7 +171,6 @@ export class OAuth2AuthProvider implements OAuthHandlers { accessToken: result.accessToken, scope: result.params.scope, expiresInSeconds: result.params.expires_in, - refreshToken: result.refreshToken, }, profile, }; diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index fe4c042500..e5bfd14f40 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -112,34 +112,31 @@ export class OidcAuthProvider implements OAuthHandlers { return await executeRedirectStrategy(req, strategy, options); } - async handler( - req: express.Request, - ): Promise<{ response: OAuthResponse; refreshToken?: string }> { + async handler(req: express.Request) { const { strategy } = await this.implementation; - const strategyResponse = await executeFrameHandlerStrategy< + const { result, privateInfo } = await executeFrameHandlerStrategy< OidcAuthResult, PrivateInfo >(req, strategy); - const { - result: { userinfo, tokenset }, - privateInfo, - } = strategyResponse; - const identityResponse = await this.handleResult({ tokenset, userinfo }); return { - response: identityResponse, + response: await this.handleResult(result), refreshToken: privateInfo.refreshToken, }; } - async refresh(req: OAuthRefreshRequest): Promise { + async refresh(req: OAuthRefreshRequest) { const { client } = await this.implementation; const tokenset = await client.refresh(req.refreshToken); if (!tokenset.access_token) { throw new Error('Refresh failed'); } - const profile = await client.userinfo(tokenset.access_token); - return this.handleResult({ tokenset, userinfo: profile }); + const userinfo = await client.userinfo(tokenset.access_token); + + return { + response: await this.handleResult({ tokenset, userinfo }), + refreshToken: tokenset.refresh_token, + }; } private async setupStrategy(options: Options): Promise { @@ -190,7 +187,6 @@ export class OidcAuthProvider implements OAuthHandlers { providerInfo: { idToken: result.tokenset.id_token, accessToken: result.tokenset.access_token!, - refreshToken: result.tokenset.refresh_token, scope: result.tokenset.scope!, expiresInSeconds: result.tokenset.expires_in, }, diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 69bbbd8c84..1c74e171ad 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -133,9 +133,7 @@ export class OktaAuthProvider implements OAuthHandlers { }); } - async handler( - req: express.Request, - ): Promise<{ response: OAuthResponse; refreshToken: string }> { + async handler(req: express.Request) { const { result, privateInfo } = await executeFrameHandlerStrategy< OAuthResult, PrivateInfo @@ -147,7 +145,7 @@ export class OktaAuthProvider implements OAuthHandlers { }; } - async refresh(req: OAuthRefreshRequest): Promise { + async refresh(req: OAuthRefreshRequest) { const { accessToken, refreshToken, params } = await executeRefreshTokenStrategy( this._strategy, @@ -160,12 +158,14 @@ export class OktaAuthProvider implements OAuthHandlers { accessToken, ); - return this.handleResult({ - fullProfile, - params, - accessToken, + return { + response: await this.handleResult({ + fullProfile, + params, + accessToken, + }), refreshToken, - }); + }; } private async handleResult(result: OAuthResult) { @@ -177,7 +177,6 @@ export class OktaAuthProvider implements OAuthHandlers { accessToken: result.accessToken, scope: result.params.scope, expiresInSeconds: result.params.expires_in, - refreshToken: result.refreshToken, }, profile, }; diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index 8cb06ea7a7..df20982f30 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -112,9 +112,7 @@ export class OneLoginProvider implements OAuthHandlers { }); } - async handler( - req: express.Request, - ): Promise<{ response: OAuthResponse; refreshToken: string }> { + async handler(req: express.Request) { const { result, privateInfo } = await executeFrameHandlerStrategy< OAuthResult, PrivateInfo @@ -126,23 +124,27 @@ export class OneLoginProvider implements OAuthHandlers { }; } - async refresh(req: OAuthRefreshRequest): Promise { - const { accessToken, params } = await executeRefreshTokenStrategy( - this._strategy, - req.refreshToken, - req.scope, - ); + async refresh(req: OAuthRefreshRequest) { + const { accessToken, refreshToken, params } = + await executeRefreshTokenStrategy( + this._strategy, + req.refreshToken, + req.scope, + ); const fullProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, ); - return this.handleResult({ - fullProfile, - params, - accessToken, - }); + return { + response: await this.handleResult({ + fullProfile, + params, + accessToken, + }), + refreshToken, + }; } private async handleResult(result: OAuthResult) { From 613ad12960e53ce842e6a79574a62f7d6c04c890 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Tue, 28 Dec 2021 09:30:37 -0700 Subject: [PATCH 13/24] Add changeset Signed-off-by: Tim Hansen --- .changeset/modern-waves-lay.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/modern-waves-lay.md diff --git a/.changeset/modern-waves-lay.md b/.changeset/modern-waves-lay.md new file mode 100644 index 0000000000..c0d9804379 --- /dev/null +++ b/.changeset/modern-waves-lay.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Add a comment to the default backend about the fallback 404 handler. From 77a5d0fb6fce6e388f16b08b54c3bc72e260f476 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 28 Dec 2021 17:37:55 +0100 Subject: [PATCH 14/24] auth-backend: let adapter populate identity token for auth0 and onelogin Signed-off-by: Patrik Oldsberg --- plugins/auth-backend/src/providers/auth0/provider.ts | 11 ++--------- .../auth-backend/src/providers/onelogin/provider.ts | 11 ++--------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index 583f95c621..f14d1b8fd5 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -178,10 +178,7 @@ export class Auth0AuthProvider implements OAuthHandlers { } } -const defaultSignInResolver: SignInResolver = async ( - info, - ctx, -) => { +const defaultSignInResolver: SignInResolver = async info => { const { profile } = info; if (!profile.email) { @@ -190,11 +187,7 @@ const defaultSignInResolver: SignInResolver = async ( const id = profile.email.split('@')[0]; - const token = await ctx.tokenIssuer.issueToken({ - claims: { sub: id, ent: [`user:default/${id}`] }, - }); - - return { id, token }; + return { id, token: '' }; }; /** @public */ diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index 8cb06ea7a7..4f52e31f5b 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -176,10 +176,7 @@ export class OneLoginProvider implements OAuthHandlers { } } -const defaultSignInResolver: SignInResolver = async ( - info, - ctx, -) => { +const defaultSignInResolver: SignInResolver = async info => { const { profile } = info; if (!profile.email) { @@ -188,11 +185,7 @@ const defaultSignInResolver: SignInResolver = async ( const id = profile.email.split('@')[0]; - const token = await ctx.tokenIssuer.issueToken({ - claims: { sub: id, ent: [`user:default/${id}`] }, - }); - - return { id, token }; + return { id, token: '' }; }; /** @public */ From de81b7455e592e5a0ce22b5181b98a1b579516b2 Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Tue, 28 Dec 2021 20:23:14 -0500 Subject: [PATCH 15/24] feat(ApiDefinitionCard): display entity title if defined Signed-off-by: Phil Kuang --- .changeset/wet-chefs-exercise.md | 5 +++++ .../components/ApiDefinitionCard/ApiDefinitionCard.test.tsx | 6 ++++-- .../src/components/ApiDefinitionCard/ApiDefinitionCard.tsx | 5 +++-- 3 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 .changeset/wet-chefs-exercise.md diff --git a/.changeset/wet-chefs-exercise.md b/.changeset/wet-chefs-exercise.md new file mode 100644 index 0000000000..cf8a2e538e --- /dev/null +++ b/.changeset/wet-chefs-exercise.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': patch +--- + +Display entity title on `ApiDefinitionCard` if defined diff --git a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx index 32a6ec0f77..936edc07da 100644 --- a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx +++ b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx @@ -62,6 +62,7 @@ paths: kind: 'API', metadata: { name: 'my-name', + title: 'My Name', }, spec: { type: 'openapi', @@ -88,7 +89,7 @@ paths: ); await waitFor(() => { - expect(getByText(/my-name/i)).toBeInTheDocument(); + expect(getByText(/My Name/i)).toBeInTheDocument(); expect(getByText(/OpenAPI/)).toBeInTheDocument(); expect(getByText(/Raw/i)).toBeInTheDocument(); expect(getByText(/List all artists/i)).toBeInTheDocument(); @@ -101,6 +102,7 @@ paths: kind: 'API', metadata: { name: 'my-name', + title: 'My Name', }, spec: { type: 'custom-type', @@ -118,7 +120,7 @@ paths: , ); - expect(getByText(/my-name/i)).toBeInTheDocument(); + expect(getByText(/My Name/i)).toBeInTheDocument(); expect(getByText(/custom-type/i)).toBeInTheDocument(); expect( getAllByText( diff --git a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.tsx b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.tsx index 2ad5a4aca7..6f3e155816 100644 --- a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.tsx +++ b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.tsx @@ -39,10 +39,11 @@ export const ApiDefinitionCard = (_: Props) => { } const definitionWidget = getApiDefinitionWidget(entity); + const entityTitle = entity.metadata.title ?? entity.metadata.name; if (definitionWidget) { return ( - + {definitionWidget.component(entity.spec.definition)} @@ -58,7 +59,7 @@ export const ApiDefinitionCard = (_: Props) => { return ( From 80ff1a6a0e3d4a82b5befa32c8db3cbbde002e39 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Dec 2021 04:09:09 +0000 Subject: [PATCH 16/24] build(deps-dev): bump @storybook/addon-a11y from 6.3.12 to 6.4.9 Bumps [@storybook/addon-a11y](https://github.com/storybookjs/storybook/tree/HEAD/addons/a11y) from 6.3.12 to 6.4.9. - [Release notes](https://github.com/storybookjs/storybook/releases) - [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md) - [Commits](https://github.com/storybookjs/storybook/commits/v6.4.9/addons/a11y) --- updated-dependencies: - dependency-name: "@storybook/addon-a11y" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 198 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 179 insertions(+), 19 deletions(-) diff --git a/yarn.lock b/yarn.lock index 1e3570b3d6..376ba05a0c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5814,22 +5814,22 @@ integrity sha512-64WWqE40U/WwWV8iIQBseTU+b2t+SdJSyQoCLdVPCKM9uf7KOjRivVwXe4KlWoV3y7duNSGuB2UgWhkXzscVmQ== "@storybook/addon-a11y@^6.3.4": - version "6.3.12" - resolved "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-6.3.12.tgz#2f930fc84fc275a4ed43a716fc09cc12caf4e110" - integrity sha512-q1NdRHFJV6sLEEJw0hatCc5ZIthELqM/AWdrEWDyhcJNyiq7Tq4nKqQBMTQSYwHiUAmxVgw7i4oa1vM2M51/3g== + version "6.4.9" + resolved "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-6.4.9.tgz#95cd51ad7a71e4c0a39259df2eb10c382bf324db" + integrity sha512-9LwFprh7A3KWmQRTqnyh/wvQ1SX/BewzhBPWqJzq0fGnx9fG35zt0VFtXV13i157wTRDqSO1g92Dgxw9l3S8/A== dependencies: - "@storybook/addons" "6.3.12" - "@storybook/api" "6.3.12" - "@storybook/channels" "6.3.12" - "@storybook/client-api" "6.3.12" - "@storybook/client-logger" "6.3.12" - "@storybook/components" "6.3.12" - "@storybook/core-events" "6.3.12" - "@storybook/theming" "6.3.12" + "@storybook/addons" "6.4.9" + "@storybook/api" "6.4.9" + "@storybook/channels" "6.4.9" + "@storybook/client-logger" "6.4.9" + "@storybook/components" "6.4.9" + "@storybook/core-events" "6.4.9" + "@storybook/csf" "0.0.2--canary.87bc651.0" + "@storybook/theming" "6.4.9" axe-core "^4.2.0" core-js "^3.8.2" global "^4.4.0" - lodash "^4.17.20" + lodash "^4.17.21" react-sizeme "^3.0.1" regenerator-runtime "^0.13.7" ts-dedent "^2.0.0" @@ -5911,7 +5911,7 @@ global "^4.4.0" regenerator-runtime "^0.13.7" -"@storybook/addons@6.3.12", "@storybook/addons@^6.1.11": +"@storybook/addons@6.3.12": version "6.3.12" resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.3.12.tgz#8773dcc113c5086dfff722388b7b65580e43b65b" integrity sha512-UgoMyr7Qr0FS3ezt8u6hMEcHgyynQS9ucr5mAwZky3wpXRPFyUTmMto9r4BBUdqyUvTUj/LRKIcmLBfj+/l0Fg== @@ -5941,6 +5941,23 @@ global "^4.4.0" regenerator-runtime "^0.13.7" +"@storybook/addons@6.4.9", "@storybook/addons@^6.1.11": + version "6.4.9" + resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.4.9.tgz#43b5dabf6781d863fcec0a0b293c236b4d5d4433" + integrity sha512-y+oiN2zd+pbRWwkf6aQj4tPDFn+rQkrv7fiVoMxsYub+kKyZ3CNOuTSJH+A1A+eBL6DmzocChUyO6jvZFuh6Dg== + dependencies: + "@storybook/api" "6.4.9" + "@storybook/channels" "6.4.9" + "@storybook/client-logger" "6.4.9" + "@storybook/core-events" "6.4.9" + "@storybook/csf" "0.0.2--canary.87bc651.0" + "@storybook/router" "6.4.9" + "@storybook/theming" "6.4.9" + "@types/webpack-env" "^1.16.0" + core-js "^3.8.2" + global "^4.4.0" + regenerator-runtime "^0.13.7" + "@storybook/api@6.3.11": version "6.3.11" resolved "https://registry.npmjs.org/@storybook/api/-/api-6.3.11.tgz#ea3806a0570da65bfb5b39e4edb90289b5ba701e" @@ -6019,6 +6036,29 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" +"@storybook/api@6.4.9": + version "6.4.9" + resolved "https://registry.npmjs.org/@storybook/api/-/api-6.4.9.tgz#6187d08658629580f0a583f2069d55b34964b34a" + integrity sha512-U+YKcDQg8xal9sE5eSMXB9vcqk8fD1pSyewyAjjbsW5hV0B3L3i4u7z/EAD9Ujbnor+Cvxq+XGvp+Qnc5Gd40A== + dependencies: + "@storybook/channels" "6.4.9" + "@storybook/client-logger" "6.4.9" + "@storybook/core-events" "6.4.9" + "@storybook/csf" "0.0.2--canary.87bc651.0" + "@storybook/router" "6.4.9" + "@storybook/semver" "^7.3.2" + "@storybook/theming" "6.4.9" + core-js "^3.8.2" + fast-deep-equal "^3.1.3" + global "^4.4.0" + lodash "^4.17.21" + memoizerific "^1.11.3" + regenerator-runtime "^0.13.7" + store2 "^2.12.0" + telejson "^5.3.2" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + "@storybook/builder-webpack4@6.3.11": version "6.3.11" resolved "https://registry.npmjs.org/@storybook/builder-webpack4/-/builder-webpack4-6.3.11.tgz#b1b62a41b2fbd951733e86aaa4730dc2541b4221" @@ -6148,6 +6188,15 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" +"@storybook/channels@6.4.9": + version "6.4.9" + resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.4.9.tgz#132c574d3fb2e6aaa9c52312c592794699b9d8ec" + integrity sha512-DNW1qDg+1WFS2aMdGh658WJXh8xBXliO5KAn0786DKcWCsKjfsPPQg/QCHczHK0+s5SZyzQT5aOBb4kTRHELQA== + dependencies: + core-js "^3.8.2" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + "@storybook/client-api@6.3.11": version "6.3.11" resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.3.11.tgz#3e6548bf6e83a2958db701cf59740a2519eea771" @@ -6220,6 +6269,14 @@ core-js "^3.8.2" global "^4.4.0" +"@storybook/client-logger@6.4.9": + version "6.4.9" + resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.4.9.tgz#ef6af30fac861fea69c8917120ed06b4c2f0b54e" + integrity sha512-BVagmmHcuKDZ/XROADfN3tiolaDW2qG0iLmDhyV1gONnbGE6X5Qm19Jt2VYu3LvjKF1zMPSWm4mz7HtgdwKbuQ== + dependencies: + core-js "^3.8.2" + global "^4.4.0" + "@storybook/components@6.3.11": version "6.3.11" resolved "https://registry.npmjs.org/@storybook/components/-/components-6.3.11.tgz#a7d015fc9808d0200d033d8f694db79277770030" @@ -6310,6 +6367,36 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" +"@storybook/components@6.4.9": + version "6.4.9" + resolved "https://registry.npmjs.org/@storybook/components/-/components-6.4.9.tgz#caed59eb3f09d1646da748186f718a0e54fb8fd7" + integrity sha512-uOUR97S6kjptkMCh15pYNM1vAqFXtpyneuonmBco5vADJb3ds0n2a8NeVd+myIbhIXn55x0OHKiSwBH/u7swCQ== + dependencies: + "@popperjs/core" "^2.6.0" + "@storybook/client-logger" "6.4.9" + "@storybook/csf" "0.0.2--canary.87bc651.0" + "@storybook/theming" "6.4.9" + "@types/color-convert" "^2.0.0" + "@types/overlayscrollbars" "^1.12.0" + "@types/react-syntax-highlighter" "11.0.5" + color-convert "^2.0.1" + core-js "^3.8.2" + fast-deep-equal "^3.1.3" + global "^4.4.0" + lodash "^4.17.21" + markdown-to-jsx "^7.1.3" + memoizerific "^1.11.3" + overlayscrollbars "^1.13.1" + polished "^4.0.5" + prop-types "^15.7.2" + react-colorful "^5.1.2" + react-popper-tooltip "^3.1.1" + react-syntax-highlighter "^13.5.3" + react-textarea-autosize "^8.3.0" + regenerator-runtime "^0.13.7" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + "@storybook/core-client@6.3.11": version "6.3.11" resolved "https://registry.npmjs.org/@storybook/core-client/-/core-client-6.3.11.tgz#beede8dfb0b0d86945f0b15ef574c6ed8af37576" @@ -6408,6 +6495,13 @@ dependencies: core-js "^3.8.2" +"@storybook/core-events@6.4.9": + version "6.4.9" + resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.4.9.tgz#7febedb8d263fbd6e4a69badbfcdce0101e6f782" + integrity sha512-YhU2zJr6wzvh5naYYuy/0UKNJ/SaXu73sIr0Tx60ur3bL08XkRg7eZ9vBhNBTlAa35oZqI0iiGCh0ljiX7yEVQ== + dependencies: + core-js "^3.8.2" + "@storybook/core-server@6.3.11": version "6.3.11" resolved "https://registry.npmjs.org/@storybook/core-server/-/core-server-6.3.11.tgz#e6cfdab8de72254007de50473186734c26bbcbc7" @@ -6483,6 +6577,13 @@ dependencies: lodash "^4.17.15" +"@storybook/csf@0.0.2--canary.87bc651.0": + version "0.0.2--canary.87bc651.0" + resolved "https://registry.npmjs.org/@storybook/csf/-/csf-0.0.2--canary.87bc651.0.tgz#c7b99b3a344117ef67b10137b6477a3d2750cf44" + integrity sha512-ajk1Uxa+rBpFQHKrCcTmJyQBXZ5slfwHVEaKlkuFaW77it8RgbPJp/ccna3sgoi8oZ7FkkOyvv1Ve4SmwFqRqw== + dependencies: + lodash "^4.17.15" + "@storybook/manager-webpack4@6.3.11": version "6.3.11" resolved "https://registry.npmjs.org/@storybook/manager-webpack4/-/manager-webpack4-6.3.11.tgz#477a796da09a771b2d3e1adfe217a79de37057af" @@ -6627,6 +6728,23 @@ qs "^6.10.0" ts-dedent "^2.0.0" +"@storybook/router@6.4.9": + version "6.4.9" + resolved "https://registry.npmjs.org/@storybook/router/-/router-6.4.9.tgz#7cc3f85494f4e14d38925e2802145df69a071201" + integrity sha512-GT2KtVHo/mBjxDBFB5ZtVJVf8vC+3p5kRlQC4jao68caVp7H24ikPOkcY54VnQwwe4A1aXpGbJXUyTisEPFlhQ== + dependencies: + "@storybook/client-logger" "6.4.9" + core-js "^3.8.2" + fast-deep-equal "^3.1.3" + global "^4.4.0" + history "5.0.0" + lodash "^4.17.21" + memoizerific "^1.11.3" + qs "^6.10.0" + react-router "^6.0.0" + react-router-dom "^6.0.0" + ts-dedent "^2.0.0" + "@storybook/semver@^7.3.2": version "7.3.2" resolved "https://registry.npmjs.org/@storybook/semver/-/semver-7.3.2.tgz#f3b9c44a1c9a0b933c04e66d0048fcf2fa10dac0" @@ -6705,6 +6823,24 @@ resolve-from "^5.0.0" ts-dedent "^2.0.0" +"@storybook/theming@6.4.9": + version "6.4.9" + resolved "https://registry.npmjs.org/@storybook/theming/-/theming-6.4.9.tgz#8ece44007500b9a592e71eca693fbeac90803b0d" + integrity sha512-Do6GH6nKjxfnBg6djcIYAjss5FW9SRKASKxLYxX2RyWJBpz0m/8GfcGcRyORy0yFTk6jByA3Hs+WFH3GnEbWkw== + dependencies: + "@emotion/core" "^10.1.1" + "@emotion/is-prop-valid" "^0.8.6" + "@emotion/styled" "^10.0.27" + "@storybook/client-logger" "6.4.9" + core-js "^3.8.2" + deep-object-diff "^1.1.0" + emotion-theming "^10.0.27" + global "^4.4.0" + memoizerific "^1.11.3" + polished "^4.0.5" + resolve-from "^5.0.0" + ts-dedent "^2.0.0" + "@storybook/ui@6.3.11": version "6.3.11" resolved "https://registry.npmjs.org/@storybook/ui/-/ui-6.3.11.tgz#fc04d6ab50b78dc5f3d8fdc1eade0c78e4c4a4e9" @@ -10006,12 +10142,7 @@ aws4@^1.11.0, aws4@^1.8.0: resolved "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== -axe-core@^4.0.2: - version "4.1.3" - resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.1.3.tgz#64a4c85509e0991f5168340edc4bedd1ceea6966" - integrity sha512-vwPpH4Aj4122EW38mxO/fxhGKtwWTMLDIJfZ1He0Edbtjcfna/R3YB67yVhezUMzqc3Jr3+Ii50KRntlENL4xQ== - -axe-core@^4.2.0: +axe-core@^4.0.2, axe-core@^4.2.0: version "4.3.1" resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.3.1.tgz#0c6a076e4a1c3e0544ba6a9479158f9be7a7928e" integrity sha512-3WVgVPs/7OnKU3s+lqMtkv3wQlg3WxK1YifmpJSDO0E1aPBrZWlrrTO6cxRqCXLuX2aYgCljqXIQd0VnRidV0g== @@ -16645,6 +16776,13 @@ highlight.js@^10.7.2, highlight.js@~10.7.0: resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== +history@5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/history/-/history-5.0.0.tgz#0cabbb6c4bbf835addb874f8259f6d25101efd08" + integrity sha512-3NyRMKIiFSJmIPdq7FxkNMJkQ7ZEtVblOQ38VtKaA0zZMW1Eo6Q6W8oDKEflr1kNNTItSnk4JMCO1deeSgbLLg== + dependencies: + "@babel/runtime" "^7.7.6" + history@^5.0.0: version "5.1.0" resolved "https://registry.npmjs.org/history/-/history-5.1.0.tgz#2e93c09c064194d38d52ed62afd0afc9d9b01ece" @@ -16652,6 +16790,13 @@ history@^5.0.0: dependencies: "@babel/runtime" "^7.7.6" +history@^5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/history/-/history-5.2.0.tgz#7cdd31cf9bac3c5d31f09c231c9928fad0007b7c" + integrity sha512-uPSF6lAJb3nSePJ43hN3eKj1dTWpN9gMod0ZssbFTIsen+WehTmEadgL+kg78xLJFdRfrrC//SavDzmRVdE+Ig== + dependencies: + "@babel/runtime" "^7.7.6" + hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -24651,6 +24796,14 @@ react-router-dom@6.0.0-beta.0, react-router-dom@^6.0.0-beta.0: prop-types "^15.7.2" react-router "6.0.0-beta.0" +react-router-dom@^6.0.0: + version "6.2.1" + resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.2.1.tgz#32ec81829152fbb8a7b045bf593a22eadf019bec" + integrity sha512-I6Zax+/TH/cZMDpj3/4Fl2eaNdcvoxxHoH1tYOREsQ22OKDYofGebrNm6CTPUcvLvZm63NL/vzCYdjf9CUhqmA== + dependencies: + history "^5.2.0" + react-router "6.2.1" + react-router@6.0.0-beta.0, react-router@^6.0.0-beta.0: version "6.0.0-beta.0" resolved "https://registry.npmjs.org/react-router/-/react-router-6.0.0-beta.0.tgz#3e11f39b6ded4412c2fed9e4f989dd4c8156724d" @@ -24658,6 +24811,13 @@ react-router@6.0.0-beta.0, react-router@^6.0.0-beta.0: dependencies: prop-types "^15.7.2" +react-router@6.2.1, react-router@^6.0.0: + version "6.2.1" + resolved "https://registry.npmjs.org/react-router/-/react-router-6.2.1.tgz#be2a97a6006ce1d9123c28934e604faef51448a3" + integrity sha512-2fG0udBtxou9lXtK97eJeET2ki5//UWfQSl1rlJ7quwe6jrktK9FCCc8dQb5QY6jAv3jua8bBQRhhDOM/kVRsg== + dependencies: + history "^5.2.0" + react-side-effect@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.0.tgz#1ce4a8b4445168c487ed24dab886421f74d380d3" From 3e1eec852f077ff67c7b061c0f674283016dd695 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Dec 2021 04:15:14 +0000 Subject: [PATCH 17/24] build(deps): bump prop-types from 15.7.2 to 15.8.0 Bumps [prop-types](https://github.com/facebook/prop-types) from 15.7.2 to 15.8.0. - [Release notes](https://github.com/facebook/prop-types/releases) - [Changelog](https://github.com/facebook/prop-types/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/prop-types/compare/v15.7.2...v15.8.0) --- updated-dependencies: - dependency-name: prop-types dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index 1e3570b3d6..ad284777da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23948,13 +23948,13 @@ promzard@^0.3.0: read "1" prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: - version "15.7.2" - resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" - integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== + version "15.8.0" + resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.0.tgz#d237e624c45a9846e469f5f31117f970017ff588" + integrity sha512-fDGekdaHh65eI3lMi5OnErU6a8Ighg2KjcjQxO7m8VHyWjcPyj5kiOgV1LQDOOOgVy3+5FgjXvdSSX7B8/5/4g== dependencies: loose-envify "^1.4.0" object-assign "^4.1.1" - react-is "^16.8.1" + react-is "^16.13.1" property-expr@^2.0.4: version "2.0.4" @@ -24549,7 +24549,7 @@ react-inspector@^5.1.0, react-inspector@^5.1.1: is-dom "^1.0.0" prop-types "^15.0.0" -react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0: +react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== From 17cb793aed009e6e61b3a03a4dbc2362f8d414c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Dec 2021 04:18:36 +0000 Subject: [PATCH 18/24] build(deps-dev): bump start-server-and-test from 1.13.1 to 1.14.0 Bumps [start-server-and-test](https://github.com/bahmutov/start-server-and-test) from 1.13.1 to 1.14.0. - [Release notes](https://github.com/bahmutov/start-server-and-test/releases) - [Commits](https://github.com/bahmutov/start-server-and-test/compare/v1.13.1...v1.14.0) --- updated-dependencies: - dependency-name: start-server-and-test dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 1e3570b3d6..a1623c0f4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26785,9 +26785,9 @@ stacktrace-js@^2.0.2: stacktrace-gps "^3.0.4" start-server-and-test@^1.10.11: - version "1.13.1" - resolved "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-1.13.1.tgz#c06eb18c3f31d610724722b7eecbdf2550b03582" - integrity sha512-wZjksmjG5scEHXmV/3HWzImxNzUgaNQ6W8kkqL2GbiOldM+nqiqh7niimlC9ZGNopTGj16kheWZnZtSWgdBZNQ== + version "1.14.0" + resolved "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-1.14.0.tgz#c57f04f73eac15dd51733b551d775b40837fdde3" + integrity sha512-on5ELuxO2K0t8EmNj9MtVlFqwBMxfWOhu4U7uZD1xccVpFlOQKR93CSe0u98iQzfNxRyaNTb/CdadbNllplTsw== dependencies: bluebird "3.7.2" check-more-types "2.24.0" From 9d9cfc1b8aacd8e3753eb22572b91e74772cecb5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 29 Dec 2021 11:45:20 +0100 Subject: [PATCH 19/24] app-backend: disallow all iframe embedding of the app Signed-off-by: Patrik Oldsberg --- .changeset/honest-chefs-mate.md | 5 +++++ plugins/app-backend/package.json | 1 + plugins/app-backend/src/service/router.ts | 3 +++ 3 files changed, 9 insertions(+) create mode 100644 .changeset/honest-chefs-mate.md diff --git a/.changeset/honest-chefs-mate.md b/.changeset/honest-chefs-mate.md new file mode 100644 index 0000000000..dfc1ff16fb --- /dev/null +++ b/.changeset/honest-chefs-mate.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app-backend': patch +--- + +Set `X-Frame-Options: deny` rather than the default `sameorigin` for all content served by the `app-backend`.` diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index fffc34ee17..58469b27fe 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -38,6 +38,7 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "fs-extra": "9.1.0", + "helmet": "^4.0.0", "winston": "^3.2.1", "yn": "^4.0.0" }, diff --git a/plugins/app-backend/src/service/router.ts b/plugins/app-backend/src/service/router.ts index 297029dffc..171684a964 100644 --- a/plugins/app-backend/src/service/router.ts +++ b/plugins/app-backend/src/service/router.ts @@ -16,6 +16,7 @@ import { notFoundHandler, resolvePackagePath } from '@backstage/backend-common'; import { Config } from '@backstage/config'; +import helmet from 'helmet'; import express from 'express'; import Router from 'express-promise-router'; import fs from 'fs-extra'; @@ -89,6 +90,8 @@ export async function createRouter( const router = Router(); + router.use(helmet.frameguard({ action: 'deny' })); + // Use a separate router for static content so that a fallback can be provided by backend const staticRouter = Router(); staticRouter.use(express.static(resolvePath(appDistDir, 'static'))); From 916b2f1f3efeb3e8c0fe917ff6d532e501bead0f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 29 Dec 2021 11:47:44 +0100 Subject: [PATCH 20/24] backend-common: get default csp policy from helmet Signed-off-by: Patrik Oldsberg --- .changeset/eight-worms-report.md | 5 +++++ .../src/service/lib/ServiceBuilderImpl.ts | 19 ++++--------------- .../backend-common/src/service/lib/config.ts | 2 -- 3 files changed, 9 insertions(+), 17 deletions(-) create mode 100644 .changeset/eight-worms-report.md diff --git a/.changeset/eight-worms-report.md b/.changeset/eight-worms-report.md new file mode 100644 index 0000000000..1dc12db0e4 --- /dev/null +++ b/.changeset/eight-worms-report.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Use the default CSP policy provided by `helmet` directly rather than a copy. diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index 54f145539f..5d33206684 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -19,6 +19,7 @@ import compression from 'compression'; import cors from 'cors'; import express, { Router, ErrorRequestHandler } from 'express'; import helmet from 'helmet'; +import { ContentSecurityPolicyOptions } from 'helmet/dist/middlewares/content-security-policy'; import * as http from 'http'; import stoppable from 'stoppable'; import { Logger } from 'winston'; @@ -43,19 +44,6 @@ import { createHttpServer, createHttpsServer } from './hostFactory'; export const DEFAULT_PORT = 7007; // '' is express default, which listens to all interfaces const DEFAULT_HOST = ''; -// taken from the helmet source code - don't seem to be exported -const DEFAULT_CSP = { - 'default-src': ["'self'"], - 'base-uri': ["'self'"], - 'block-all-mixed-content': [], - 'font-src': ["'self'", 'https:', 'data:'], - 'frame-ancestors': ["'self'"], - 'img-src': ["'self'", 'data:'], - 'object-src': ["'none'"], - 'script-src': ["'self'", "'unsafe-eval'"], - 'script-src-attr': ["'none'"], - 'style-src': ["'self'", 'https:', "'unsafe-inline'"], -}; export class ServiceBuilderImpl implements ServiceBuilder { private port: number | undefined; @@ -236,8 +224,9 @@ export class ServiceBuilderImpl implements ServiceBuilder { export function applyCspDirectives( directives: Record | undefined, -): CspOptions | undefined { - const result: CspOptions = { ...DEFAULT_CSP }; +): ContentSecurityPolicyOptions['directives'] { + const result: ContentSecurityPolicyOptions['directives'] = + helmet.contentSecurityPolicy.getDefaultDirectives(); if (directives) { for (const [key, value] of Object.entries(directives)) { diff --git a/packages/backend-common/src/service/lib/config.ts b/packages/backend-common/src/service/lib/config.ts index d69b00f04c..91eeff13aa 100644 --- a/packages/backend-common/src/service/lib/config.ts +++ b/packages/backend-common/src/service/lib/config.ts @@ -42,8 +42,6 @@ export type CertificateAttributes = { /** * A map from CSP directive names to their values. - * - * Added here since helmet doesn't export this type publicly. */ export type CspOptions = Record; From a6bc757a4540b6afc338d557a9f17339eb2d0a19 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 29 Dec 2021 11:54:30 +0100 Subject: [PATCH 21/24] Review notes. Signed-off-by: Eric Peterson --- .../SearchContextForStorybook.stories.tsx | 5 +++- .../SearchType/SearchType.Accordion.test.tsx | 26 ++++++++++++++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx b/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx index 4528a6e46d..e969e0d8cc 100644 --- a/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx +++ b/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx @@ -25,7 +25,10 @@ type QueryResultProps = { }; /** - * Utility context provider only for use in Storybook stories. + * Utility context provider only for use in Storybook stories. You should use + * the real `` exported by `@backstage/plugin-search` in + * your app instead of this! In some cases (like the search page) it may + * already be provided on your behalf. */ export const SearchContextProvider = ( props: ComponentProps & QueryResultProps, diff --git a/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx index d56b40b879..6b4171a024 100644 --- a/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx @@ -15,13 +15,19 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; +import { ApiProvider } from '@backstage/core-app-api'; +import { TestApiRegistry } from '@backstage/test-utils'; +import { act, render } from '@testing-library/react'; import user from '@testing-library/user-event'; -import { SearchContext } from '../SearchContext'; +import { searchApiRef } from '../../apis'; +import { SearchContext, SearchContextProvider } from '../SearchContext'; import { SearchType } from './SearchType'; describe('SearchType.Accordion', () => { + const query = jest.fn(); + const mockApis = TestApiRegistry.from([searchApiRef, { query }]); + const contextSpy = { result: { loading: false, value: { results: [] } }, term: '', @@ -42,14 +48,20 @@ describe('SearchType.Accordion', () => { }; beforeEach(() => { + query.mockResolvedValue({ results: [] }); + }); + + afterEach(() => { jest.resetAllMocks(); }); - it('should render as expected', () => { + it('should render as expected', async () => { const { getByText } = render( - - - , + + + + + , ); // The given label should be rendered. @@ -63,6 +75,8 @@ describe('SearchType.Accordion', () => { // The given type is also visible expect(getByText(expectedType.name)).toBeInTheDocument(); + + await act(() => Promise.resolve()); }); it('should set entire types array when a type is selected', () => { From 20af5a701f6954314ec468c8d1ff06022f9e2f39 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 29 Dec 2021 12:10:23 +0100 Subject: [PATCH 22/24] Update create-app with search accordion component. Signed-off-by: Eric Peterson --- .changeset/search-over-tysta-skogor.md | 58 +++++++++++++++++++ .../app/src/components/search/SearchPage.tsx | 30 ++++++++-- 2 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 .changeset/search-over-tysta-skogor.md diff --git a/.changeset/search-over-tysta-skogor.md b/.changeset/search-over-tysta-skogor.md new file mode 100644 index 0000000000..5398ae7f5f --- /dev/null +++ b/.changeset/search-over-tysta-skogor.md @@ -0,0 +1,58 @@ +--- +'@backstage/create-app': patch +--- + +The `` filter in the composed `SearchPage.tsx` was replaced with the `` variant. + +This is an entirely optional change; if you wish to display a control surface for search `types` as a single-select accordion (as opposed to the current multi-select of checkboxes), you can make the following (or similar) changes to your search page layout: + +```diff +--- a/packages/app/src/components/search/SearchPage.tsx ++++ b/packages/app/src/components/search/SearchPage.tsx +@@ -11,7 +11,7 @@ import { + SearchType, + DefaultResultListItem, + } from '@backstage/plugin-search'; +-import { Content, Header, Page } from '@backstage/core-components'; ++import { CatalogIcon, Content, DocsIcon, Header, Page } from '@backstage/core-components'; + + const useStyles = makeStyles((theme: Theme) => ({ + bar: { +@@ -19,6 +19,7 @@ const useStyles = makeStyles((theme: Theme) => ({ + }, + filters: { + padding: theme.spacing(2), ++ marginTop: theme.spacing(2), + }, + filter: { + '& + &': { +@@ -41,12 +42,23 @@ const SearchPage = () => { + + + ++ , ++ }, ++ { ++ value: 'techdocs', ++ name: 'Documentation', ++ icon: , ++ }, ++ ]} ++ /> + +- + ({ bar: { @@ -19,6 +25,7 @@ const useStyles = makeStyles((theme: Theme) => ({ }, filters: { padding: theme.spacing(2), + marginTop: theme.spacing(2), }, filter: { '& + &': { @@ -41,12 +48,23 @@ const SearchPage = () => { + , + }, + { + value: 'techdocs', + name: 'Documentation', + icon: , + }, + ]} + /> - Date: Wed, 29 Dec 2021 14:03:30 +0100 Subject: [PATCH 23/24] backend-test-utils: lazy-load testcontainers Signed-off-by: Patrik Oldsberg --- .changeset/beige-wolves-kneel.md | 5 +++++ .../backend-test-utils/src/database/startMysqlContainer.ts | 4 +++- .../src/database/startPostgresContainer.ts | 4 +++- 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 .changeset/beige-wolves-kneel.md diff --git a/.changeset/beige-wolves-kneel.md b/.changeset/beige-wolves-kneel.md new file mode 100644 index 0000000000..2c768a3f03 --- /dev/null +++ b/.changeset/beige-wolves-kneel.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Lazy-load `testcontainers` module in order to avoid side-effects. diff --git a/packages/backend-test-utils/src/database/startMysqlContainer.ts b/packages/backend-test-utils/src/database/startMysqlContainer.ts index 9601854cb3..fa99389cbc 100644 --- a/packages/backend-test-utils/src/database/startMysqlContainer.ts +++ b/packages/backend-test-utils/src/database/startMysqlContainer.ts @@ -15,7 +15,6 @@ */ import createConnection, { Knex } from 'knex'; -import { GenericContainer } from 'testcontainers'; import { v4 as uuid } from 'uuid'; async function waitForMysqlReady( @@ -50,6 +49,9 @@ export async function startMysqlContainer(image: string) { const user = 'root'; const password = uuid(); + // Lazy-load to avoid side-effect of importing testcontainers + const { GenericContainer } = await import('testcontainers'); + const container = await new GenericContainer(image) .withExposedPorts(3306) .withEnv('MYSQL_ROOT_PASSWORD', password) diff --git a/packages/backend-test-utils/src/database/startPostgresContainer.ts b/packages/backend-test-utils/src/database/startPostgresContainer.ts index 89a0417e22..81358e01d3 100644 --- a/packages/backend-test-utils/src/database/startPostgresContainer.ts +++ b/packages/backend-test-utils/src/database/startPostgresContainer.ts @@ -15,7 +15,6 @@ */ import createConnection, { Knex } from 'knex'; -import { GenericContainer } from 'testcontainers'; import { v4 as uuid } from 'uuid'; async function waitForPostgresReady( @@ -50,6 +49,9 @@ export async function startPostgresContainer(image: string) { const user = 'postgres'; const password = uuid(); + // Lazy-load to avoid side-effect of importing testcontainers + const { GenericContainer } = await import('testcontainers'); + const container = await new GenericContainer(image) .withExposedPorts(5432) .withEnv('POSTGRES_PASSWORD', password) From af4980fb5d304fe0ec49f85fbe89b5c5e4c83d63 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Wed, 29 Dec 2021 14:10:01 +0100 Subject: [PATCH 24/24] [Search] SearchBar Analytics tracking (#8656) * test capture search bar events Signed-off-by: Emma Indal * capture search bar events Signed-off-by: Emma Indal * only run onChange method if provided, otherwise set term Signed-off-by: Emma Indal * changeset Signed-off-by: Emma Indal * unconditionally call analytics API Signed-off-by: Emma Indal * update changeset to be less specific to GA Signed-off-by: Emma Indal * move analytics tracking to SearchBarBase Signed-off-by: Emma Indal * add search to key events Signed-off-by: Emma Indal * capture types as analytics context attribute, only for SearchBar Signed-off-by: Emma Indal * move AnalyticsContext to within SearchContextProvider Signed-off-by: Emma Indal * refactor search tracking out to its own component Signed-off-by: Emma Indal * captures not only google analytics Signed-off-by: Emma Indal --- .changeset/search-odd-starfishes-raise.md | 5 + docs/plugins/analytics.md | 9 +- .../components/SearchBar/SearchBar.test.tsx | 139 +++++++++++++++++- .../src/components/SearchBar/SearchBar.tsx | 34 +++-- .../SearchContext/SearchContext.tsx | 8 +- .../SearchTracker/SearchTracker.tsx | 36 +++++ .../src/components/SearchTracker/index.ts | 16 ++ 7 files changed, 223 insertions(+), 24 deletions(-) create mode 100644 .changeset/search-odd-starfishes-raise.md create mode 100644 plugins/search/src/components/SearchTracker/SearchTracker.tsx create mode 100644 plugins/search/src/components/SearchTracker/index.ts diff --git a/.changeset/search-odd-starfishes-raise.md b/.changeset/search-odd-starfishes-raise.md new file mode 100644 index 0000000000..73627cf457 --- /dev/null +++ b/.changeset/search-odd-starfishes-raise.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search': patch +--- + +Captures the search term entered in the SearchBarBase as a `search` event. diff --git a/docs/plugins/analytics.md b/docs/plugins/analytics.md index ace9a89968..7fbd256e86 100644 --- a/docs/plugins/analytics.md +++ b/docs/plugins/analytics.md @@ -55,10 +55,11 @@ learn how to contribute the integration yourself! The following table summarizes events that, depending on the plugins you have installed, may be captured. -| Action | Provided By | Subject | -| ---------- | -------------- | ----------------------------------------- | -| `navigate` | Backstage Core | The URL of the page that was navigated to | -| `click` | Backstage Core | The text of the link that was clicked on | +| Action | Provided By | Subject | +| ---------- | -------------- | --------------------------------------------------- | +| `navigate` | Backstage Core | The URL of the page that was navigated to | +| `click` | Backstage Core | The text of the link that was clicked on | +| `search` | Backstage Core | The search term entered in any search bar component | If there is an event you'd like to see captured, please [open an issue][add-event] describing the event you want to see and the questions it diff --git a/plugins/search/src/components/SearchBar/SearchBar.test.tsx b/plugins/search/src/components/SearchBar/SearchBar.test.tsx index b3f30473ac..cb4008d494 100644 --- a/plugins/search/src/components/SearchBar/SearchBar.test.tsx +++ b/plugins/search/src/components/SearchBar/SearchBar.test.tsx @@ -20,10 +20,10 @@ import userEvent from '@testing-library/user-event'; import { SearchContextProvider } from '../SearchContext'; import { SearchBar } from './SearchBar'; -import { configApiRef } from '@backstage/core-plugin-api'; +import { configApiRef, analyticsApiRef } from '@backstage/core-plugin-api'; import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { searchApiRef } from '../../apis'; -import { TestApiRegistry } from '@backstage/test-utils'; +import { MockAnalyticsApi, TestApiRegistry } from '@backstage/test-utils'; jest.mock('@backstage/core-plugin-api', () => ({ ...jest.requireActual('@backstage/core-plugin-api'), @@ -38,9 +38,16 @@ describe('SearchBar', () => { }; const query = jest.fn().mockResolvedValue({}); + const analyticsApiSpy = new MockAnalyticsApi(); + let apiRegistry: TestApiRegistry; - const apiRegistry = TestApiRegistry.from( - [configApiRef, new ConfigReader({ app: { title: 'Mock title' } })], + apiRegistry = TestApiRegistry.from( + [ + configApiRef, + new ConfigReader({ + app: { title: 'Mock title' }, + }), + ], [searchApiRef, { query }], ); @@ -210,4 +217,128 @@ describe('SearchBar', () => { expect.objectContaining({ term: value }), ); }); + + it('does not capture analytics event if not enabled in app', async () => { + jest.useFakeTimers(); + + const debounceTime = 600; + + render( + + + + + , + , + ); + + await waitFor(() => { + expect(screen.getByRole('textbox', { name })).toBeInTheDocument(); + }); + + const textbox = screen.getByRole('textbox', { name }); + + const value = 'value'; + + userEvent.type(textbox, value); + + act(() => { + jest.advanceTimersByTime(debounceTime); + }); + + await waitFor(() => expect(textbox).toHaveValue(value)); + + expect(analyticsApiSpy.getEvents()).toHaveLength(0); + }); + + it('captures analytics events if enabled in app', async () => { + jest.useFakeTimers(); + + const debounceTime = 600; + + apiRegistry = TestApiRegistry.from( + [analyticsApiRef, analyticsApiSpy], + [ + configApiRef, + new ConfigReader({ + app: { + title: 'Mock title', + analytics: { + ga: { + trackingId: 'xyz123', + }, + }, + }, + }), + ], + [searchApiRef, { query }], + ); + + render( + + + + + , + ); + + await waitFor(() => { + expect(screen.getByRole('textbox', { name })).toBeInTheDocument(); + }); + + const textbox = screen.getByRole('textbox', { name }); + + const value = 'value'; + + userEvent.type(textbox, value); + + expect(analyticsApiSpy.getEvents()).toHaveLength(0); + + act(() => { + jest.advanceTimersByTime(debounceTime); + }); + + await waitFor(() => expect(textbox).toHaveValue(value)); + + expect(analyticsApiSpy.getEvents()).toHaveLength(1); + expect(analyticsApiSpy.getEvents()[0]).toEqual({ + action: 'search', + context: { + extension: 'App', + pluginId: 'root', + routeRef: 'unknown', + searchTypes: 'software-catalog,techdocs', + }, + subject: 'value', + }); + + userEvent.clear(textbox); + + // make sure new term is captured + userEvent.type(textbox, 'new value'); + + act(() => { + jest.advanceTimersByTime(debounceTime); + }); + + await waitFor(() => expect(textbox).toHaveValue('new value')); + + expect(analyticsApiSpy.getEvents()).toHaveLength(2); + expect(analyticsApiSpy.getEvents()[1]).toEqual({ + action: 'search', + context: { + extension: 'App', + pluginId: 'root', + routeRef: 'unknown', + searchTypes: 'software-catalog,techdocs', + }, + subject: 'new value', + }); + }); }); diff --git a/plugins/search/src/components/SearchBar/SearchBar.tsx b/plugins/search/src/components/SearchBar/SearchBar.tsx index 95bf1570e1..aa4b1e728a 100644 --- a/plugins/search/src/components/SearchBar/SearchBar.tsx +++ b/plugins/search/src/components/SearchBar/SearchBar.tsx @@ -33,6 +33,7 @@ import SearchIcon from '@material-ui/icons/Search'; import ClearButton from '@material-ui/icons/Clear'; import { useSearch } from '../SearchContext'; +import { TrackSearch } from '../SearchTracker'; /** * Props for {@link SearchBarBase}. @@ -119,18 +120,20 @@ export const SearchBarBase = ({ ); return ( - + + + ); }; @@ -150,8 +153,11 @@ export const SearchBar = ({ onChange, ...props }: SearchBarProps) => { const { term, setTerm } = useSearch(); const handleChange = (newValue: string) => { - setTerm(newValue); - if (onChange) onChange(newValue); + if (onChange) { + onChange(newValue); + } else { + setTerm(newValue); + } }; return ; diff --git a/plugins/search/src/components/SearchContext/SearchContext.tsx b/plugins/search/src/components/SearchContext/SearchContext.tsx index dd9310d782..300a191983 100644 --- a/plugins/search/src/components/SearchContext/SearchContext.tsx +++ b/plugins/search/src/components/SearchContext/SearchContext.tsx @@ -15,7 +15,7 @@ */ import { JsonObject } from '@backstage/types'; -import { useApi } from '@backstage/core-plugin-api'; +import { useApi, AnalyticsContext } from '@backstage/core-plugin-api'; import { SearchResultSet } from '@backstage/search-common'; import React, { createContext, @@ -130,7 +130,11 @@ export const SearchContextProvider = ({ fetchPreviousPage: hasPreviousPage ? fetchPreviousPage : undefined, }; - return ; + return ( + + + + ); }; export const useSearch = () => { diff --git a/plugins/search/src/components/SearchTracker/SearchTracker.tsx b/plugins/search/src/components/SearchTracker/SearchTracker.tsx new file mode 100644 index 0000000000..b9da9b5560 --- /dev/null +++ b/plugins/search/src/components/SearchTracker/SearchTracker.tsx @@ -0,0 +1,36 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useEffect } from 'react'; +import { useAnalytics } from '@backstage/core-plugin-api'; +import { useSearch } from '../SearchContext'; + +/** + * Capture search event on term change. + */ +export const TrackSearch = ({ children }: { children: React.ReactChild }) => { + const analytics = useAnalytics(); + const { term } = useSearch(); + + useEffect(() => { + if (term) { + // Capture analytics search event with search term provided as value + analytics.captureEvent('search', term); + } + }, [analytics, term]); + + return <>{children}; +}; diff --git a/plugins/search/src/components/SearchTracker/index.ts b/plugins/search/src/components/SearchTracker/index.ts new file mode 100644 index 0000000000..5e6a75aa25 --- /dev/null +++ b/plugins/search/src/components/SearchTracker/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { TrackSearch } from './SearchTracker';