diff --git a/packages/backend-common/src/setupTests.ts b/packages/backend-common/src/setupTests.ts index 3fa7cb04b4..a3b2b7e123 100644 --- a/packages/backend-common/src/setupTests.ts +++ b/packages/backend-common/src/setupTests.ts @@ -15,3 +15,4 @@ */ require('jest-fetch-mock').enableMocks(); +export {}; diff --git a/packages/backend/src/index.test.ts b/packages/backend/src/index.test.ts index b3e2f19771..d18873c1f0 100644 --- a/packages/backend/src/index.test.ts +++ b/packages/backend/src/index.test.ts @@ -14,8 +14,11 @@ * limitations under the License. */ +import { PluginEnvironment } from './types'; + describe('test', () => { it('unbreaks the test runner', () => { - expect(true).toBeTruthy(); + const unbreaker = {} as PluginEnvironment; + expect(unbreaker).toBeTruthy(); }); }); diff --git a/packages/catalog-model/src/EntityPolicies.ts b/packages/catalog-model/src/EntityPolicies.ts index cedf0df13d..fa25334d5f 100644 --- a/packages/catalog-model/src/EntityPolicies.ts +++ b/packages/catalog-model/src/EntityPolicies.ts @@ -17,7 +17,7 @@ import { Entity, FieldFormatEntityPolicy, - ForeignRootFieldsEntityPolicy, + NoForeignRootFieldsEntityPolicy, ReservedFieldsEntityPolicy, SchemaValidEntityPolicy, } from './entity'; @@ -29,10 +29,10 @@ import { EntityPolicy } from './types'; class AllEntityPolicies implements EntityPolicy { constructor(private readonly policies: EntityPolicy[]) {} - async apply(entity: Entity): Promise { + async enforce(entity: Entity): Promise { let result = entity; for (const policy of this.policies) { - result = await policy.apply(entity); + result = await policy.enforce(entity); } return result; } @@ -43,10 +43,10 @@ class AllEntityPolicies implements EntityPolicy { class AnyEntityPolicy implements EntityPolicy { constructor(private readonly policies: EntityPolicy[]) {} - async apply(entity: Entity): Promise { + async enforce(entity: Entity): Promise { for (const policy of this.policies) { try { - return await policy.apply(entity); + return await policy.enforce(entity); } catch { continue; } @@ -62,7 +62,7 @@ export class EntityPolicies implements EntityPolicy { return EntityPolicies.allOf([ EntityPolicies.allOf([ new SchemaValidEntityPolicy(), - new ForeignRootFieldsEntityPolicy(), + new NoForeignRootFieldsEntityPolicy(), new FieldFormatEntityPolicy(), new ReservedFieldsEntityPolicy(), ]), @@ -82,7 +82,7 @@ export class EntityPolicies implements EntityPolicy { this.policy = policy; } - apply(entity: Entity): Promise { - return this.policy.apply(entity); + enforce(entity: Entity): Promise { + return this.policy.enforce(entity); } } diff --git a/packages/catalog-model/src/entity/Entity.ts b/packages/catalog-model/src/entity/Entity.ts index 6f57626749..80c5765ee1 100644 --- a/packages/catalog-model/src/entity/Entity.ts +++ b/packages/catalog-model/src/entity/Entity.ts @@ -32,9 +32,9 @@ export type Entity = { kind: string; /** - * Optional metadata related to the entity. + * Metadata related to the entity. */ - metadata?: EntityMeta; + metadata: EntityMeta; /** * The specification data describing the entity itself. @@ -86,9 +86,9 @@ export type EntityMeta = { * The name of the entity. * * Must be uniqe within the catalog at any given point in time, for any - * given namespace, for any given kind. + * given namespace + kind pair. */ - name?: string; + name: string; /** * The namespace that the entity belongs to. diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts index d81b1155be..14b44108e5 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts @@ -42,64 +42,64 @@ describe('FieldFormatEntityPolicy', () => { }); it('works for the happy path', async () => { - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('rejects bad apiVersion', async () => { data.apiVersion = 7; - await expect(policy.apply(data)).rejects.toThrow(/apiVersion/); + await expect(policy.enforce(data)).rejects.toThrow(/apiVersion/); data.apiVersion = 'a#b'; - await expect(policy.apply(data)).rejects.toThrow(/apiVersion/); + await expect(policy.enforce(data)).rejects.toThrow(/apiVersion/); }); it('rejects bad kind', async () => { data.kind = 7; - await expect(policy.apply(data)).rejects.toThrow(/kind/); + await expect(policy.enforce(data)).rejects.toThrow(/kind/); data.kind = 'a#b'; - await expect(policy.apply(data)).rejects.toThrow(/kind/); + await expect(policy.enforce(data)).rejects.toThrow(/kind/); }); it('handles missing metadata gracefully', async () => { delete data.medatata; - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('handles missing spec gracefully', async () => { delete data.spec; - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('rejects bad name', async () => { data.metadata.name = 7; - await expect(policy.apply(data)).rejects.toThrow(/name.*7/); + await expect(policy.enforce(data)).rejects.toThrow(/name.*7/); data.metadata.name = 'a'.repeat(1000); - await expect(policy.apply(data)).rejects.toThrow(/name.*aaaa/); + await expect(policy.enforce(data)).rejects.toThrow(/name.*aaaa/); }); it('rejects bad namespace', async () => { data.metadata.namespace = 7; - await expect(policy.apply(data)).rejects.toThrow(/namespace.*7/); + await expect(policy.enforce(data)).rejects.toThrow(/namespace.*7/); data.metadata.namespace = 'a'.repeat(1000); - await expect(policy.apply(data)).rejects.toThrow(/namespace.*aaaa/); + await expect(policy.enforce(data)).rejects.toThrow(/namespace.*aaaa/); }); it('rejects bad label key', async () => { data.metadata.labels['a#b'] = 'value'; - await expect(policy.apply(data)).rejects.toThrow(/label.*a#b/i); + await expect(policy.enforce(data)).rejects.toThrow(/label.*a#b/i); }); it('rejects bad label value', async () => { data.metadata.labels.a = 'a#b'; - await expect(policy.apply(data)).rejects.toThrow(/label.*a#b/i); + await expect(policy.enforce(data)).rejects.toThrow(/label.*a#b/i); }); it('rejects bad annotation key', async () => { data.metadata.annotations['a#b'] = 'value'; - await expect(policy.apply(data)).rejects.toThrow(/annotation.*a#b/i); + await expect(policy.enforce(data)).rejects.toThrow(/annotation.*a#b/i); }); it('rejects bad annotation value', async () => { data.metadata.annotations.a = 7; - await expect(policy.apply(data)).rejects.toThrow(/annotation.*7/i); + await expect(policy.enforce(data)).rejects.toThrow(/annotation.*7/i); }); }); diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts index 1f94354f3f..4ccd8ec711 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts @@ -32,7 +32,7 @@ export class FieldFormatEntityPolicy implements EntityPolicy { this.validators = validators; } - async apply(entity: Entity): Promise { + async enforce(entity: Entity): Promise { function require( field: string, value: any, @@ -65,23 +65,20 @@ export class FieldFormatEntityPolicy implements EntityPolicy { require('apiVersion', entity.apiVersion, this.validators.isValidApiVersion); require('kind', entity.kind, this.validators.isValidKind); - optional( - 'metadata.name', - entity.metadata?.name, - this.validators.isValidEntityName, - ); + require('metadata.name', entity.metadata.name, this.validators + .isValidEntityName); optional( 'metadata.namespace', - entity.metadata?.namespace, + entity.metadata.namespace, this.validators.isValidNamespace, ); - for (const [k, v] of Object.entries(entity.metadata?.labels ?? [])) { + for (const [k, v] of Object.entries(entity.metadata.labels ?? [])) { require(`labels.${k}`, k, this.validators.isValidLabelKey); require(`labels.${k}`, v, this.validators.isValidLabelValue); } - for (const [k, v] of Object.entries(entity.metadata?.annotations ?? [])) { + for (const [k, v] of Object.entries(entity.metadata.annotations ?? [])) { require(`annotations.${k}`, k, this.validators.isValidAnnotationKey); require(`annotations.${k}`, v, this.validators.isValidAnnotationValue); } diff --git a/packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.test.ts similarity index 77% rename from packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.test.ts rename to packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.test.ts index 98299259f8..50496682e8 100644 --- a/packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.test.ts @@ -15,11 +15,11 @@ */ import yaml from 'yaml'; -import { ForeignRootFieldsEntityPolicy } from './ForeignRootFieldsEntityPolicy'; +import { NoForeignRootFieldsEntityPolicy } from './NoForeignRootFieldsEntityPolicy'; -describe('ForeignRootFieldsEntityPolicy', () => { +describe('NoForeignRootFieldsEntityPolicy', () => { let data: any; - let policy: ForeignRootFieldsEntityPolicy; + let policy: NoForeignRootFieldsEntityPolicy; beforeEach(() => { data = yaml.parse(` @@ -38,15 +38,15 @@ describe('ForeignRootFieldsEntityPolicy', () => { spec: custom: stuff `); - policy = new ForeignRootFieldsEntityPolicy(); + policy = new NoForeignRootFieldsEntityPolicy(); }); it('works for the happy path', async () => { - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('rejects unknown root fields', async () => { data.spec2 = {}; - await expect(policy.apply(data)).rejects.toThrow(/spec2/i); + await expect(policy.enforce(data)).rejects.toThrow(/spec2/i); }); }); diff --git a/packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.ts b/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.ts similarity index 90% rename from packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.ts rename to packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.ts index a4733e9a42..9d1851bc02 100644 --- a/packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.ts @@ -22,14 +22,14 @@ const defaultKnownFields = ['apiVersion', 'kind', 'metadata', 'spec']; /** * Ensures that there are no foreign root fields in the entity. */ -export class ForeignRootFieldsEntityPolicy implements EntityPolicy { +export class NoForeignRootFieldsEntityPolicy implements EntityPolicy { private readonly knownFields: string[]; constructor(knownFields: string[] = defaultKnownFields) { this.knownFields = knownFields; } - async apply(entity: Entity): Promise { + async enforce(entity: Entity): Promise { for (const field of Object.keys(entity)) { if (!this.knownFields.includes(field)) { throw new Error(`Unknown field ${field}`); diff --git a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts index 348eabbdac..8db33955a7 100644 --- a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts @@ -42,21 +42,23 @@ describe('ReservedFieldsEntityPolicy', () => { }); it('works for the happy path', async () => { - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('rejects reserved keys in the spec root', async () => { data.spec.apiVersion = 'a/b'; - await expect(policy.apply(data)).rejects.toThrow(/spec.*apiVersion/i); + await expect(policy.enforce(data)).rejects.toThrow(/spec.*apiVersion/i); }); it('rejects reserved keys in labels', async () => { data.metadata.labels.apiVersion = 'a'; - await expect(policy.apply(data)).rejects.toThrow(/label.*apiVersion/i); + await expect(policy.enforce(data)).rejects.toThrow(/label.*apiVersion/i); }); it('rejects reserved keys in annotations', async () => { data.metadata.annotations.apiVersion = 'a'; - await expect(policy.apply(data)).rejects.toThrow(/annotation.*apiVersion/i); + await expect(policy.enforce(data)).rejects.toThrow( + /annotation.*apiVersion/i, + ); }); }); diff --git a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts index be2f732ca4..57029ffa24 100644 --- a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts @@ -43,19 +43,19 @@ export class ReservedFieldsEntityPolicy implements EntityPolicy { ]; } - async apply(entity: Entity): Promise { + async enforce(entity: Entity): Promise { for (const field of this.reservedFields) { if (entity.spec?.hasOwnProperty(field)) { throw new Error( `The spec may not contain the field ${field}, because it has reserved meaning`, ); } - if (entity.metadata?.labels?.hasOwnProperty(field)) { + if (entity.metadata.labels?.hasOwnProperty(field)) { throw new Error( `A label may not have the field ${field}, because it has reserved meaning`, ); } - if (entity.metadata?.annotations?.hasOwnProperty(field)) { + if (entity.metadata.annotations?.hasOwnProperty(field)) { throw new Error( `An annotation may not have the field ${field}, because it has reserved meaning`, ); diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts index b9d1165a60..c53fbf3e46 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts @@ -43,7 +43,7 @@ describe('SchemaValidEntityPolicy', () => { }); it('works for the happy path', async () => { - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); // @@ -51,113 +51,113 @@ describe('SchemaValidEntityPolicy', () => { // it('rejects wrong root type', async () => { - await expect(policy.apply((7 as unknown) as Entity)).rejects.toThrow( + await expect(policy.enforce((7 as unknown) as Entity)).rejects.toThrow( /object/, ); }); it('rejects missing apiVersion', async () => { delete data.apiVersion; - await expect(policy.apply(data)).rejects.toThrow(/apiVersion/); + await expect(policy.enforce(data)).rejects.toThrow(/apiVersion/); }); it('rejects bad apiVersion type', async () => { data.apiVersion = 7; - await expect(policy.apply(data)).rejects.toThrow(/apiVersion/); + await expect(policy.enforce(data)).rejects.toThrow(/apiVersion/); }); it('rejects missing kind', async () => { delete data.kind; - await expect(policy.apply(data)).rejects.toThrow(/kind/); + await expect(policy.enforce(data)).rejects.toThrow(/kind/); }); it('rejects bad kind type', async () => { data.kind = 7; - await expect(policy.apply(data)).rejects.toThrow(/kind/); + await expect(policy.enforce(data)).rejects.toThrow(/kind/); }); // // metadata // - it('accepts missing metadata', async () => { - delete data.medatata; - await expect(policy.apply(data)).resolves.toBe(data); + it('rejects missing metadata', async () => { + delete data.metadata; + await expect(policy.enforce(data)).rejects.toThrow(/metadata/); }); it('rejects bad metadata type', async () => { data.metadata = 7; - await expect(policy.apply(data)).rejects.toThrow(/metadata/); + await expect(policy.enforce(data)).rejects.toThrow(/metadata/); }); it('accepts missing uid', async () => { delete data.metadata.uid; - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('rejects bad uid type', async () => { data.metadata.uid = 7; - await expect(policy.apply(data)).rejects.toThrow(/uid/); + await expect(policy.enforce(data)).rejects.toThrow(/uid/); }); it('accepts missing etag', async () => { delete data.metadata.etag; - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('rejects bad etag type', async () => { data.metadata.etag = 7; - await expect(policy.apply(data)).rejects.toThrow(/etag/); + await expect(policy.enforce(data)).rejects.toThrow(/etag/); }); it('accepts missing generation', async () => { delete data.metadata.generation; - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('rejects bad generation type', async () => { data.metadata.generation = 'a'; - await expect(policy.apply(data)).rejects.toThrow(/generation/); + await expect(policy.enforce(data)).rejects.toThrow(/generation/); }); - it('accepts missing name', async () => { + it('rejects missing name', async () => { delete data.metadata.name; - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).rejects.toThrow(/name/); }); it('rejects bad name type', async () => { data.metadata.name = 7; - await expect(policy.apply(data)).rejects.toThrow(/name/); + await expect(policy.enforce(data)).rejects.toThrow(/name/); }); it('accepts missing namespace', async () => { delete data.metadata.namespace; - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('rejects bad namespace type', async () => { data.metadata.namespace = 7; - await expect(policy.apply(data)).rejects.toThrow(/namespace/); + await expect(policy.enforce(data)).rejects.toThrow(/namespace/); }); it('accepts missing labels', async () => { delete data.metadata.labels; - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('rejects bad labels type', async () => { data.metadata.labels = 7; - await expect(policy.apply(data)).rejects.toThrow(/labels/); + await expect(policy.enforce(data)).rejects.toThrow(/labels/); }); it('accepts missing annotations', async () => { delete data.metadata.annotations; - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('rejects bad annotations type', async () => { data.metadata.annotations = 7; - await expect(policy.apply(data)).rejects.toThrow(/annotations/); + await expect(policy.enforce(data)).rejects.toThrow(/annotations/); }); // @@ -166,11 +166,11 @@ describe('SchemaValidEntityPolicy', () => { it('accepts missing spec', async () => { delete data.spec; - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('rejects non-object spec', async () => { data.spec = 7; - await expect(policy.apply(data)).rejects.toThrow(/spec/); + await expect(policy.enforce(data)).rejects.toThrow(/spec/); }); }); diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts index 7c0f5c20b6..d367022ff1 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts @@ -47,12 +47,12 @@ const DEFAULT_ENTITY_SCHEMA = yup.object({ 'The generation must be an integer greater than zero', value => value === undefined || (value === (value | 0) && value > 0), ), - name: yup.string().notRequired(), + name: yup.string().required(), namespace: yup.string().notRequired(), labels: yup.object>().notRequired(), annotations: yup.object>().notRequired(), }) - .notRequired(), + .required(), spec: yup.object({}).notRequired(), }); @@ -70,7 +70,7 @@ export class SchemaValidEntityPolicy implements EntityPolicy { this.schema = schema; } - async apply(entity: Entity): Promise { + async enforce(entity: Entity): Promise { try { return await this.schema.validate(entity, { strict: true }); } catch (e) { diff --git a/packages/catalog-model/src/entity/policies/index.ts b/packages/catalog-model/src/entity/policies/index.ts index f43aa68049..d64053f7cb 100644 --- a/packages/catalog-model/src/entity/policies/index.ts +++ b/packages/catalog-model/src/entity/policies/index.ts @@ -15,6 +15,6 @@ */ export { FieldFormatEntityPolicy } from './FieldFormatEntityPolicy'; -export { ForeignRootFieldsEntityPolicy } from './ForeignRootFieldsEntityPolicy'; +export { NoForeignRootFieldsEntityPolicy } from './NoForeignRootFieldsEntityPolicy'; export { ReservedFieldsEntityPolicy } from './ReservedFieldsEntityPolicy'; export { SchemaValidEntityPolicy } from './SchemaValidEntityPolicy'; diff --git a/packages/catalog-model/src/kinds/ComponentV1beta1.ts b/packages/catalog-model/src/kinds/ComponentV1beta1.ts index b0a3f627a5..b041bf7967 100644 --- a/packages/catalog-model/src/kinds/ComponentV1beta1.ts +++ b/packages/catalog-model/src/kinds/ComponentV1beta1.ts @@ -50,7 +50,7 @@ export class ComponentV1beta1Policy implements EntityPolicy { }); } - async apply(envelope: Entity): Promise { + async enforce(envelope: Entity): Promise { if ( envelope.apiVersion !== 'backstage.io/v1beta1' || envelope.kind !== 'Component' diff --git a/packages/catalog-model/src/types.ts b/packages/catalog-model/src/types.ts index 1d581cf23f..29ca8bdfa3 100644 --- a/packages/catalog-model/src/types.ts +++ b/packages/catalog-model/src/types.ts @@ -28,5 +28,5 @@ export type EntityPolicy = { * @returns The incoming entity, or a mutated version of the same * @throws An error if the entity should be rejected */ - apply(entity: Entity): Promise; + enforce(entity: Entity): Promise; }; diff --git a/packages/core/src/api/createApp.tsx b/packages/core/src/api-wrappers/createApp.tsx similarity index 97% rename from packages/core/src/api/createApp.tsx rename to packages/core/src/api-wrappers/createApp.tsx index d0deed667a..03adc41ec6 100644 --- a/packages/core/src/api/createApp.tsx +++ b/packages/core/src/api-wrappers/createApp.tsx @@ -21,7 +21,7 @@ import privateExports, { defaultSystemIcons, } from '@backstage/core-api'; -import ErrorPage from '../layout/ErrorPage'; +import { ErrorPage } from '../layout/ErrorPage'; import { lightTheme, darkTheme } from '@backstage/theme'; const { PrivateAppImpl } = privateExports; diff --git a/packages/core/src/api/index.ts b/packages/core/src/api-wrappers/index.ts similarity index 100% rename from packages/core/src/api/index.ts rename to packages/core/src/api-wrappers/index.ts diff --git a/packages/core/src/components/CodeSnippet/CodeSnippet.stories.tsx b/packages/core/src/components/CodeSnippet/CodeSnippet.stories.tsx index 801836bd48..f7c9ba836d 100644 --- a/packages/core/src/components/CodeSnippet/CodeSnippet.stories.tsx +++ b/packages/core/src/components/CodeSnippet/CodeSnippet.stories.tsx @@ -16,7 +16,7 @@ import React from 'react'; import CodeSnippet from './CodeSnippet'; -import InfoCard from '../../layout/InfoCard'; +import { InfoCard } from '../../layout/InfoCard'; export default { title: 'CodeSnippet', diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx b/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx index 3dc5854ab3..92db2073d3 100644 --- a/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx +++ b/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx @@ -16,33 +16,10 @@ import React from 'react'; import CopyTextButton from '.'; -import { - ApiProvider, - errorApiRef, - ApiRegistry, - ErrorApi, -} from '@backstage/core-api'; export default { title: 'CopyTextButton', component: CopyTextButton, - decorators: [ - (storyFn: () => JSX.Element) => { - // TODO: move this to common storybook config, requires core package to be separate from components - const registry = ApiRegistry.from([ - [ - errorApiRef, - { - post(error) { - // eslint-disable-next-line no-alert - window.alert(`Component posted error, ${error}`); - }, - } as ErrorApi, - ], - ]); - return ; - }, - ], }; export const Default = () => ( diff --git a/packages/core/src/components/ProgressBars/ProgressCard.tsx b/packages/core/src/components/ProgressBars/ProgressCard.tsx index 66dda771c3..9e70469091 100644 --- a/packages/core/src/components/ProgressBars/ProgressCard.tsx +++ b/packages/core/src/components/ProgressBars/ProgressCard.tsx @@ -16,8 +16,8 @@ import React, { FC } from 'react'; import { makeStyles } from '@material-ui/core'; -import InfoCard from '../../layout/InfoCard'; -import { Props as BottomLinkProps } from '../../layout/BottomLink'; +import { InfoCard } from '../../layout/InfoCard'; +import { BottomLinkProps } from '../../layout/BottomLink'; import CircleProgress from './CircleProgress'; type Props = { @@ -36,7 +36,7 @@ const useStyles = makeStyles({ }, }); -const ProgressCard: FC = (props) => { +const ProgressCard: FC = props => { const classes = useStyles(props); const { title, subheader, progress, deepLink, variant } = props; diff --git a/packages/core/src/components/Status/Status.stories.tsx b/packages/core/src/components/Status/Status.stories.tsx index 140b2e6b3f..c0c579b217 100644 --- a/packages/core/src/components/Status/Status.stories.tsx +++ b/packages/core/src/components/Status/Status.stories.tsx @@ -24,7 +24,7 @@ import { StatusWarning, } from './Status'; import Table from '../Table'; -import InfoCard from '../../layout/InfoCard'; +import { InfoCard } from '../../layout/InfoCard'; export default { title: 'Status', diff --git a/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx b/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx index 334219c881..47908cba79 100644 --- a/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx +++ b/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import React, { FC } from 'react'; -import InfoCard from '../../layout/InfoCard'; +import { InfoCard } from '../../layout/InfoCard'; import { Grid } from '@material-ui/core'; import StructuredMetadataTable from '.'; diff --git a/packages/core/src/components/TrendLine/TrendLine.stories.tsx b/packages/core/src/components/TrendLine/TrendLine.stories.tsx index 506fc4e387..4a0c6ead00 100644 --- a/packages/core/src/components/TrendLine/TrendLine.stories.tsx +++ b/packages/core/src/components/TrendLine/TrendLine.stories.tsx @@ -17,7 +17,7 @@ import React from 'react'; import TrendLine from '.'; import Table from '../Table'; -import InfoCard from '../../layout/InfoCard'; +import { InfoCard } from '../../layout/InfoCard'; export default { title: 'TrendLine', diff --git a/packages/core/src/icons/icons.tsx b/packages/core/src/icons/icons.tsx deleted file mode 100644 index e8834a6162..0000000000 --- a/packages/core/src/icons/icons.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * 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 { SvgIconProps } from '@material-ui/core'; -import PeopleIcon from '@material-ui/icons/People'; -import PersonIcon from '@material-ui/icons/Person'; -import React, { FC } from 'react'; -import { useApp } from '@backstage/core-api'; -import { IconComponent, SystemIconKey, SystemIcons } from './types'; - -export const defaultSystemIcons: SystemIcons = { - user: PersonIcon, - group: PeopleIcon, -}; - -const overridableSystemIcon = (key: SystemIconKey): IconComponent => { - const Component: FC = props => { - const app = useApp(); - const Icon = app.getSystemIcon(key); - return ; - }; - return Component; -}; - -export const UserIcon = overridableSystemIcon('user'); -export const GroupIcon = overridableSystemIcon('group'); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index be9cdc432d..2ceea470f4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -16,22 +16,11 @@ export * from '@backstage/core-api'; -export * from './api'; -export { default as Page } from './layout/Page'; -export { gradients, pageTheme } from './layout/Page'; -export type { PageTheme } from './layout/Page'; +export * from './api-wrappers'; +export * from './layout'; + export { default as CodeSnippet } from './components/CodeSnippet'; -export { default as Content } from './layout/Content/Content'; -export { default as ContentHeader } from './layout/ContentHeader/ContentHeader'; export { default as DismissableBanner } from './components/DismissableBanner'; -export { default as Header } from './layout/Header/Header'; -export { default as HeaderLabel } from './layout/HeaderLabel'; -export { default as HomepageTimer } from './layout/HomepageTimer'; -export { default as InfoCard } from './layout/InfoCard'; -export { CardTab, TabbedCard } from './layout/TabbedCard'; -export { default as ErrorBoundary } from './layout/ErrorBoundary'; -export * from './layout/Sidebar'; -export * from './layout/LoginPage'; export { AlertDisplay } from './components/AlertDisplay'; export { default as HorizontalScrollGrid } from './components/HorizontalScrollGrid'; export { default as ProgressCard } from './components/ProgressBars/ProgressCard'; @@ -50,4 +39,3 @@ export { default as TrendLine } from './components/TrendLine'; export { FeatureCalloutCircular } from './components/FeatureDiscovery/FeatureCalloutCircular'; export * from './components/Status'; export { default as WarningPanel } from './components/WarningPanel'; -export type { IconComponent } from './icons'; diff --git a/packages/core/src/layout/BottomLink/BottomLink.test.tsx b/packages/core/src/layout/BottomLink/BottomLink.test.tsx index f301de1c84..9871e18ebb 100644 --- a/packages/core/src/layout/BottomLink/BottomLink.test.tsx +++ b/packages/core/src/layout/BottomLink/BottomLink.test.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { wrapInTestApp } from '@backstage/test-utils'; -import BottomLink from './BottomLink'; +import { BottomLink } from './BottomLink'; const minProps = { title: 'A deepLink title', diff --git a/packages/core/src/layout/BottomLink/BottomLink.tsx b/packages/core/src/layout/BottomLink/BottomLink.tsx index 88c723fd08..b3f7b4aa11 100644 --- a/packages/core/src/layout/BottomLink/BottomLink.tsx +++ b/packages/core/src/layout/BottomLink/BottomLink.tsx @@ -41,13 +41,13 @@ const useStyles = makeStyles(theme => ({ }, })); -export type Props = { +export type BottomLinkProps = { link: string; title: string; onClick?: (event: React.MouseEvent) => void; }; -const BottomLink: FC = ({ link, title, onClick }) => { +export const BottomLink: FC = ({ link, title, onClick }) => { const classes = useStyles(); return ( @@ -68,5 +68,3 @@ const BottomLink: FC = ({ link, title, onClick }) => { ); }; - -export default BottomLink; diff --git a/packages/core/src/layout/BottomLink/index.ts b/packages/core/src/layout/BottomLink/index.ts index 6f2055f4f7..18befcc9bf 100644 --- a/packages/core/src/layout/BottomLink/index.ts +++ b/packages/core/src/layout/BottomLink/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export { default } from './BottomLink'; -export type { Props } from './BottomLink'; +export { BottomLink } from './BottomLink'; +export type { BottomLinkProps } from './BottomLink'; diff --git a/packages/core/src/layout/Content/Content.tsx b/packages/core/src/layout/Content/Content.tsx index 38db7c88f7..6a5b2c241a 100644 --- a/packages/core/src/layout/Content/Content.tsx +++ b/packages/core/src/layout/Content/Content.tsx @@ -42,7 +42,7 @@ type Props = { className?: string; }; -const Content: FC = ({ +export const Content: FC = ({ className, stretch, noPadding, @@ -62,5 +62,3 @@ const Content: FC = ({ ); }; - -export default Content; diff --git a/packages/core/src/icons/index.ts b/packages/core/src/layout/Content/index.ts similarity index 92% rename from packages/core/src/icons/index.ts rename to packages/core/src/layout/Content/index.ts index 4c97d27176..d05ae0e787 100644 --- a/packages/core/src/icons/index.ts +++ b/packages/core/src/layout/Content/index.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -export * from './icons'; -export * from './types'; +export { Content } from './Content'; diff --git a/packages/core/src/layout/ContentHeader/ContentHeader.test.tsx b/packages/core/src/layout/ContentHeader/ContentHeader.test.tsx index 703c27e32c..2f40511f35 100644 --- a/packages/core/src/layout/ContentHeader/ContentHeader.test.tsx +++ b/packages/core/src/layout/ContentHeader/ContentHeader.test.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import ContentHeader from './ContentHeader'; +import { ContentHeader } from './ContentHeader'; import { wrapInThemedTestApp } from '@backstage/test-utils'; jest.mock('react-helmet', () => { diff --git a/packages/core/src/layout/ContentHeader/ContentHeader.tsx b/packages/core/src/layout/ContentHeader/ContentHeader.tsx index 1a51d5d19b..691c7d11f7 100644 --- a/packages/core/src/layout/ContentHeader/ContentHeader.tsx +++ b/packages/core/src/layout/ContentHeader/ContentHeader.tsx @@ -75,7 +75,7 @@ type ContentHeaderProps = { description?: string; }; -const ContentHeader: FC = ({ +export const ContentHeader: FC = ({ description, title, titleComponent: TitleComponent = undefined, @@ -105,5 +105,3 @@ const ContentHeader: FC = ({ ); }; - -export default ContentHeader; diff --git a/packages/cli/src/index.test.ts b/packages/core/src/layout/ContentHeader/index.ts similarity index 88% rename from packages/cli/src/index.test.ts rename to packages/core/src/layout/ContentHeader/index.ts index 44f01f05e9..18d03228a3 100644 --- a/packages/cli/src/index.test.ts +++ b/packages/core/src/layout/ContentHeader/index.ts @@ -14,8 +14,4 @@ * limitations under the License. */ -describe('dummy', () => { - it('dummy', () => { - expect(1).toBe(1); - }); -}); +export { ContentHeader } from './ContentHeader'; diff --git a/packages/core/src/layout/ErrorBoundary/ErrorBoundary.tsx b/packages/core/src/layout/ErrorBoundary/ErrorBoundary.tsx index 88ae99c3c9..fd267777a0 100644 --- a/packages/core/src/layout/ErrorBoundary/ErrorBoundary.tsx +++ b/packages/core/src/layout/ErrorBoundary/ErrorBoundary.tsx @@ -26,7 +26,7 @@ type State = { errorInfo?: ErrorInfo; }; -const ErrorBoundary: ComponentClass< +export const ErrorBoundary: ComponentClass< Props, State > = class ErrorBoundary extends Component { @@ -57,8 +57,6 @@ const ErrorBoundary: ComponentClass< } }; -export default ErrorBoundary; - type EProps = { error?: Error; slackChannel?: string; diff --git a/packages/core/src/layout/ErrorBoundary/index.ts b/packages/core/src/layout/ErrorBoundary/index.ts index e126359bf3..607634e89a 100644 --- a/packages/core/src/layout/ErrorBoundary/index.ts +++ b/packages/core/src/layout/ErrorBoundary/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { default } from './ErrorBoundary'; +export { ErrorBoundary } from './ErrorBoundary'; diff --git a/packages/core/src/layout/ErrorPage/ErrorPage.test.tsx b/packages/core/src/layout/ErrorPage/ErrorPage.test.tsx index 06c12041b9..d383c3fbf0 100644 --- a/packages/core/src/layout/ErrorPage/ErrorPage.test.tsx +++ b/packages/core/src/layout/ErrorPage/ErrorPage.test.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import ErrorPage from './ErrorPage'; +import { ErrorPage } from './ErrorPage'; import { wrapInThemedTestApp } from '@backstage/test-utils'; describe('', () => { diff --git a/packages/core/src/layout/ErrorPage/ErrorPage.tsx b/packages/core/src/layout/ErrorPage/ErrorPage.tsx index 747f2f6036..b94c7409fc 100644 --- a/packages/core/src/layout/ErrorPage/ErrorPage.tsx +++ b/packages/core/src/layout/ErrorPage/ErrorPage.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { Typography, Link, Grid } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; import { BackstageTheme } from '@backstage/theme'; -import MicDrop from './MicDrop'; +import { MicDrop } from './MicDrop'; import { useHistory } from 'react-router'; interface IErrorPageProps { @@ -38,7 +38,7 @@ const useStyles = makeStyles(theme => ({ }, })); -const ErrorPage = ({ status, statusMessage }: IErrorPageProps) => { +export const ErrorPage = ({ status, statusMessage }: IErrorPageProps) => { const classes = useStyles(); const history = useHistory(); @@ -63,5 +63,3 @@ const ErrorPage = ({ status, statusMessage }: IErrorPageProps) => { ); }; - -export default ErrorPage; diff --git a/packages/core/src/layout/ErrorPage/MicDrop.jsx b/packages/core/src/layout/ErrorPage/MicDrop.jsx index f951830574..4c4098f486 100644 --- a/packages/core/src/layout/ErrorPage/MicDrop.jsx +++ b/packages/core/src/layout/ErrorPage/MicDrop.jsx @@ -26,7 +26,7 @@ const useStyles = makeStyles({ }, }); -const MicDrop = () => { +export const MicDrop = () => { const classes = useStyles(); return ( { ); }; - -export default MicDrop; diff --git a/packages/core/src/layout/ErrorPage/index.ts b/packages/core/src/layout/ErrorPage/index.ts index 872e5cce2d..506ed1f815 100644 --- a/packages/core/src/layout/ErrorPage/index.ts +++ b/packages/core/src/layout/ErrorPage/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { default } from './ErrorPage'; +export { ErrorPage } from './ErrorPage'; diff --git a/packages/core/src/layout/Header/Header.stories.tsx b/packages/core/src/layout/Header/Header.stories.tsx index e1198d7b65..700120906f 100644 --- a/packages/core/src/layout/Header/Header.stories.tsx +++ b/packages/core/src/layout/Header/Header.stories.tsx @@ -14,9 +14,9 @@ * limitations under the License. */ import React from 'react'; -import Header from '.'; -import HeaderLabel from '../HeaderLabel'; -import Page, { pageTheme } from '../Page'; +import { Header } from '.'; +import { HeaderLabel } from '../HeaderLabel'; +import { Page, pageTheme } from '../Page'; export default { title: 'Header', diff --git a/packages/core/src/layout/Header/Header.test.tsx b/packages/core/src/layout/Header/Header.test.tsx index afedf2ad35..c1f9be6cf6 100644 --- a/packages/core/src/layout/Header/Header.test.tsx +++ b/packages/core/src/layout/Header/Header.test.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { wrapInThemedTestApp } from '@backstage/test-utils'; -import Header from './Header'; +import { Header } from './Header'; jest.mock('react-helmet', () => { return { diff --git a/packages/core/src/layout/Header/Header.tsx b/packages/core/src/layout/Header/Header.tsx index 47fcc324a7..ce50a2680f 100644 --- a/packages/core/src/layout/Header/Header.tsx +++ b/packages/core/src/layout/Header/Header.tsx @@ -20,7 +20,7 @@ import { Typography, Tooltip, makeStyles } from '@material-ui/core'; import { BackstageTheme } from '@backstage/theme'; import { Theme } from '../Page/Page'; -import Waves from './Waves'; +import { Waves } from './Waves'; const useStyles = makeStyles(theme => ({ header: { @@ -193,5 +193,3 @@ export const Header: FC = ({ ); }; - -export default Header; diff --git a/packages/core/src/layout/Header/Waves.test.tsx b/packages/core/src/layout/Header/Waves.test.tsx index 2c7d463052..76bf5e913e 100644 --- a/packages/core/src/layout/Header/Waves.test.tsx +++ b/packages/core/src/layout/Header/Waves.test.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { pageTheme } from '../Page/PageThemeProvider'; -import Waves from './Waves'; +import { Waves } from './Waves'; describe('', () => { it('should render svg', () => { diff --git a/packages/core/src/layout/Header/Waves.tsx b/packages/core/src/layout/Header/Waves.tsx index f37ecf7b12..a43ef14062 100644 --- a/packages/core/src/layout/Header/Waves.tsx +++ b/packages/core/src/layout/Header/Waves.tsx @@ -35,7 +35,7 @@ type Props = { theme: PageTheme; }; -const Waves: FC = ({ theme }) => { +export const Waves: FC = ({ theme }) => { const classes = useStyles(); const [color1, color2] = theme.gradient.colors; @@ -139,5 +139,3 @@ const Waves: FC = ({ theme }) => { ); }; - -export default Waves; diff --git a/packages/core/src/layout/Header/index.ts b/packages/core/src/layout/Header/index.ts index ad1f322bd7..e0860413c9 100644 --- a/packages/core/src/layout/Header/index.ts +++ b/packages/core/src/layout/Header/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { default } from './Header'; +export { Header } from './Header'; diff --git a/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.test.tsx b/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.test.tsx index 17822228e8..a1b5740f88 100644 --- a/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.test.tsx +++ b/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.test.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react'; import { wrapInThemedTestApp, Keyboard } from '@backstage/test-utils'; -import HeaderActionMenu from './HeaderActionMenu'; +import { HeaderActionMenu } from './HeaderActionMenu'; describe('', () => { it('renders without any items and without exploding', () => { diff --git a/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.tsx b/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.tsx index 4799654cee..a1a4eb6566 100644 --- a/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.tsx +++ b/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.tsx @@ -24,7 +24,7 @@ import { Popover, ListItemTextProps, } from '@material-ui/core'; -import { default as KebabMenuIcon } from './MenuVertical'; +import { VerticalMenuIcon } from './VerticalMenuIcon'; type ActionItemProps = { label?: ListItemTextProps['primary']; @@ -66,7 +66,9 @@ export type HeaderActionMenuProps = { actionItems: ActionItemProps[]; }; -const HeaderActionMenu: FC = ({ actionItems }) => { +export const HeaderActionMenu: FC = ({ + actionItems, +}) => { const [open, setOpen] = React.useState(false); const anchorElRef = React.useRef(null); @@ -84,7 +86,7 @@ const HeaderActionMenu: FC = ({ actionItems }) => { padding: 0, }} > - + = ({ actionItems }) => { ); }; - -export default HeaderActionMenu; diff --git a/packages/core/src/layout/HeaderActionMenu/MenuVertical.tsx b/packages/core/src/layout/HeaderActionMenu/VerticalMenuIcon.tsx similarity index 92% rename from packages/core/src/layout/HeaderActionMenu/MenuVertical.tsx rename to packages/core/src/layout/HeaderActionMenu/VerticalMenuIcon.tsx index 426c70fb21..c9fde8cea2 100644 --- a/packages/core/src/layout/HeaderActionMenu/MenuVertical.tsx +++ b/packages/core/src/layout/HeaderActionMenu/VerticalMenuIcon.tsx @@ -17,11 +17,9 @@ import React from 'react'; import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon'; -const SvgMenuVertical = (props: SvgIconProps) => +export const VerticalMenuIcon = (props: SvgIconProps) => React.createElement( SvgIcon, props, , ); - -export default SvgMenuVertical; diff --git a/plugins/auth-backend/src/index.test.ts b/packages/core/src/layout/HeaderActionMenu/index.ts similarity index 84% rename from plugins/auth-backend/src/index.test.ts rename to packages/core/src/layout/HeaderActionMenu/index.ts index b3e2f19771..bb7fa80d59 100644 --- a/plugins/auth-backend/src/index.test.ts +++ b/packages/core/src/layout/HeaderActionMenu/index.ts @@ -14,8 +14,4 @@ * limitations under the License. */ -describe('test', () => { - it('unbreaks the test runner', () => { - expect(true).toBeTruthy(); - }); -}); +export { HeaderActionMenu } from './HeaderActionMenu'; diff --git a/packages/core/src/layout/HeaderLabel/HeaderLabel.test.tsx b/packages/core/src/layout/HeaderLabel/HeaderLabel.test.tsx index cbdfb3da80..11a22f9b5d 100644 --- a/packages/core/src/layout/HeaderLabel/HeaderLabel.test.tsx +++ b/packages/core/src/layout/HeaderLabel/HeaderLabel.test.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { wrapInThemedTestApp } from '@backstage/test-utils'; -import HeaderLabel from './HeaderLabel'; +import { HeaderLabel } from './HeaderLabel'; describe('', () => { it('should have a label', () => { diff --git a/packages/core/src/layout/HeaderLabel/HeaderLabel.tsx b/packages/core/src/layout/HeaderLabel/HeaderLabel.tsx index e2fd11ff15..d4457a6fba 100644 --- a/packages/core/src/layout/HeaderLabel/HeaderLabel.tsx +++ b/packages/core/src/layout/HeaderLabel/HeaderLabel.tsx @@ -56,7 +56,7 @@ type HeaderLabelProps = { url?: string; }; -const HeaderLabel: FC = ({ label, value, url }) => { +export const HeaderLabel: FC = ({ label, value, url }) => { const classes = useStyles(); const content = ( = ({ label, value, url }) => { ); }; - -export default HeaderLabel; diff --git a/packages/core/src/layout/HeaderLabel/index.ts b/packages/core/src/layout/HeaderLabel/index.ts index 8e0b7a0c10..683ec59784 100644 --- a/packages/core/src/layout/HeaderLabel/index.ts +++ b/packages/core/src/layout/HeaderLabel/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { default } from './HeaderLabel'; +export { HeaderLabel } from './HeaderLabel'; diff --git a/packages/core/src/layout/HomepageTimer/HomepageTimer.tsx b/packages/core/src/layout/HomepageTimer/HomepageTimer.tsx index 9fe0aaa344..7ea4dd508f 100644 --- a/packages/core/src/layout/HomepageTimer/HomepageTimer.tsx +++ b/packages/core/src/layout/HomepageTimer/HomepageTimer.tsx @@ -15,7 +15,7 @@ */ import React, { FC } from 'react'; -import HeaderLabel from '../HeaderLabel'; +import { HeaderLabel } from '../HeaderLabel'; const timeFormat = { hour: '2-digit', minute: '2-digit' }; const utcOptions = { timeZone: 'UTC', ...timeFormat }; @@ -44,7 +44,7 @@ function getTimes() { return { timeNY, timeUTC, timeTYO, timeSTO }; } -const HomePageTimer: FC<{}> = () => { +export const HomepageTimer: FC<{}> = () => { const [{ timeNY, timeUTC, timeTYO, timeSTO }, setTimes] = React.useState( defaultTimes, ); @@ -70,5 +70,3 @@ const HomePageTimer: FC<{}> = () => { ); }; - -export default HomePageTimer; diff --git a/packages/core/src/layout/HomepageTimer/index.ts b/packages/core/src/layout/HomepageTimer/index.ts index be260e5b88..facee1e982 100644 --- a/packages/core/src/layout/HomepageTimer/index.ts +++ b/packages/core/src/layout/HomepageTimer/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { default } from './HomepageTimer'; +export { HomepageTimer } from './HomepageTimer'; diff --git a/packages/core/src/layout/InfoCard/InfoCard.stories.tsx b/packages/core/src/layout/InfoCard/InfoCard.stories.tsx index e9fa831b40..7d888f5486 100644 --- a/packages/core/src/layout/InfoCard/InfoCard.stories.tsx +++ b/packages/core/src/layout/InfoCard/InfoCard.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import React, { FC } from 'react'; -import InfoCard from '.'; +import { InfoCard } from '.'; import { Grid } from '@material-ui/core'; const cardContentStyle = { height: 200, width: 500 }; diff --git a/packages/core/src/layout/InfoCard/InfoCard.test.tsx b/packages/core/src/layout/InfoCard/InfoCard.test.tsx index 4d8221d5b8..d58fdb2ccb 100644 --- a/packages/core/src/layout/InfoCard/InfoCard.test.tsx +++ b/packages/core/src/layout/InfoCard/InfoCard.test.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { wrapInTestApp } from '@backstage/test-utils'; -import InfoCard from './InfoCard'; +import { InfoCard } from './InfoCard'; const minProps = { title: 'Some title', diff --git a/packages/core/src/layout/InfoCard/InfoCard.tsx b/packages/core/src/layout/InfoCard/InfoCard.tsx index 9be29b5a6b..04736b64cd 100644 --- a/packages/core/src/layout/InfoCard/InfoCard.tsx +++ b/packages/core/src/layout/InfoCard/InfoCard.tsx @@ -25,8 +25,8 @@ import { makeStyles, } from '@material-ui/core'; import classNames from 'classnames'; -import ErrorBoundary from '../ErrorBoundary'; -import BottomLink, { Props as BottomLinkProps } from '../BottomLink'; +import { ErrorBoundary } from '../ErrorBoundary'; +import { BottomLink, BottomLinkProps } from '../BottomLink'; const useStyles = makeStyles(theme => ({ header: { @@ -137,7 +137,7 @@ type Props = { noPadding?: boolean; }; -const InfoCard: FC = ({ +export const InfoCard: FC = ({ title, subheader, divider, @@ -214,5 +214,3 @@ const InfoCard: FC = ({ ); }; - -export default InfoCard; diff --git a/packages/core/src/layout/InfoCard/index.ts b/packages/core/src/layout/InfoCard/index.ts index 6b715f996f..d77fcf93c4 100644 --- a/packages/core/src/layout/InfoCard/index.ts +++ b/packages/core/src/layout/InfoCard/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { default } from './InfoCard'; +export { InfoCard } from './InfoCard'; diff --git a/packages/core/src/layout/LoginPage/LoginPage.tsx b/packages/core/src/layout/LoginPage/LoginPage.tsx index e7c83d563c..7bf14dba01 100644 --- a/packages/core/src/layout/LoginPage/LoginPage.tsx +++ b/packages/core/src/layout/LoginPage/LoginPage.tsx @@ -16,10 +16,11 @@ import React, { FC, useState } from 'react'; import GitHubIcon from '@material-ui/icons/GitHub'; -import Page from '../Page'; -import Header from '../Header'; -import Content from '../Content/Content'; -import ContentHeader from '../ContentHeader/ContentHeader'; +import { Page } from '../Page'; +import { Header } from '../Header'; +import { Content } from '../Content'; +import { ContentHeader } from '../ContentHeader'; +import { InfoCard } from '../InfoCard/InfoCard'; import { Grid, Typography, @@ -29,7 +30,6 @@ import { ListItem, Link, } from '@material-ui/core'; -import InfoCard from '../InfoCard/InfoCard'; enum AuthType { GitHub, diff --git a/packages/core/src/layout/Page/Page.tsx b/packages/core/src/layout/Page/Page.tsx index 23c2a14dbe..bc4175fb6c 100644 --- a/packages/core/src/layout/Page/Page.tsx +++ b/packages/core/src/layout/Page/Page.tsx @@ -35,7 +35,7 @@ type Props = { theme?: PageTheme; }; -const Page: FC = ({ theme = pageTheme.home, children }) => { +export const Page: FC = ({ theme = pageTheme.home, children }) => { const classes = useStyles(); return ( @@ -43,5 +43,3 @@ const Page: FC = ({ theme = pageTheme.home, children }) => { ); }; - -export default Page; diff --git a/packages/core/src/layout/Page/index.ts b/packages/core/src/layout/Page/index.ts index 50899fa036..f8fa7dc668 100644 --- a/packages/core/src/layout/Page/index.ts +++ b/packages/core/src/layout/Page/index.ts @@ -14,6 +14,6 @@ * limitations under the License. */ -export { default } from './Page'; +export { Page } from './Page'; export { gradients, pageTheme } from './PageThemeProvider'; export type { PageTheme } from './PageThemeProvider'; diff --git a/packages/core/src/layout/Sidebar/Bar.tsx b/packages/core/src/layout/Sidebar/Bar.tsx index c5a8ae5669..1aadf9fd3e 100644 --- a/packages/core/src/layout/Sidebar/Bar.tsx +++ b/packages/core/src/layout/Sidebar/Bar.tsx @@ -16,10 +16,10 @@ import { makeStyles } from '@material-ui/core'; import clsx from 'clsx'; -import React, { FC, useRef, useState } from 'react'; +import React, { FC, useRef, useState, useContext } from 'react'; import { sidebarConfig, SidebarContext } from './config'; import { BackstageTheme } from '@backstage/theme'; -import { useSidebarPinState } from '../../hooks/useSidebarPinState'; +import { SidebarPinStateContext } from './Page'; const useStyles = makeStyles(theme => ({ root: { @@ -76,7 +76,7 @@ export const Sidebar: FC = ({ const classes = useStyles(); const [state, setState] = useState(State.Closed); const hoverTimerRef = useRef(); - const { isPinned } = useSidebarPinState(); + const { isPinned } = useContext(SidebarPinStateContext); const handleOpen = () => { if (isPinned) { diff --git a/packages/core/src/layout/Sidebar/Items.tsx b/packages/core/src/layout/Sidebar/Items.tsx index 4696f53a37..ddd71cc189 100644 --- a/packages/core/src/layout/Sidebar/Items.tsx +++ b/packages/core/src/layout/Sidebar/Items.tsx @@ -22,12 +22,12 @@ import { Typography, Badge, } from '@material-ui/core'; +import { IconComponent } from '@backstage/core-api'; import SearchIcon from '@material-ui/icons/Search'; import clsx from 'clsx'; import React, { FC, useContext, useState, KeyboardEventHandler } from 'react'; import { NavLink } from 'react-router-dom'; import { sidebarConfig, SidebarContext } from './config'; -import { IconComponent } from '../../icons'; const useStyles = makeStyles(theme => { const { diff --git a/packages/core/src/layout/Sidebar/Page.tsx b/packages/core/src/layout/Sidebar/Page.tsx index 750f764c2d..2a4cf0fbb2 100644 --- a/packages/core/src/layout/Sidebar/Page.tsx +++ b/packages/core/src/layout/Sidebar/Page.tsx @@ -18,7 +18,7 @@ import { makeStyles } from '@material-ui/core'; import React, { createContext, FC, useEffect, useState } from 'react'; import { sidebarConfig } from './config'; import { BackstageTheme } from '@backstage/theme'; -import { LocalStorage } from '../../data/localStorage'; +import { LocalStorage } from './localStorage'; const useStyles = makeStyles({ root: { diff --git a/packages/core/src/layout/Sidebar/index.ts b/packages/core/src/layout/Sidebar/index.ts index 731d117ce3..baefc3d0e8 100644 --- a/packages/core/src/layout/Sidebar/index.ts +++ b/packages/core/src/layout/Sidebar/index.ts @@ -14,10 +14,22 @@ * limitations under the License. */ -export * from './Bar'; -export * from './Page'; -export * from './Items'; -export * from './Intro'; -export * from './UserBadge'; -export * from './config'; +export { Sidebar } from './Bar'; +export { SidebarPage, SidebarPinStateContext } from './Page'; +export type { SidebarPinStateContextType } from './Page'; +export { + SidebarDivider, + SidebarItem, + SidebarSearchField, + SidebarSpace, + SidebarSpacer, +} from './Items'; +export { IntroCard, SidebarIntro } from './Intro'; +export { SidebarUserBadge } from './UserBadge'; +export { + SIDEBAR_INTRO_LOCAL_STORAGE, + SidebarContext, + sidebarConfig, +} from './config'; +export type { SidebarContextType } from './config'; export { SidebarThemeToggle } from './SidebarThemeToggle'; diff --git a/packages/core/src/data/localStorage.test.ts b/packages/core/src/layout/Sidebar/localStorage.test.ts similarity index 100% rename from packages/core/src/data/localStorage.test.ts rename to packages/core/src/layout/Sidebar/localStorage.test.ts diff --git a/packages/core/src/data/localStorage.ts b/packages/core/src/layout/Sidebar/localStorage.ts similarity index 100% rename from packages/core/src/data/localStorage.ts rename to packages/core/src/layout/Sidebar/localStorage.ts diff --git a/packages/core/src/layout/TabbedCard/TabbedCard.tsx b/packages/core/src/layout/TabbedCard/TabbedCard.tsx index 4dd1399fb0..e1967b8533 100644 --- a/packages/core/src/layout/TabbedCard/TabbedCard.tsx +++ b/packages/core/src/layout/TabbedCard/TabbedCard.tsx @@ -26,8 +26,8 @@ import { Tab, TabProps, } from '@material-ui/core'; -import BottomLink, { Props as BottomLinkProps } from '../BottomLink'; -import ErrorBoundary from '../ErrorBoundary/ErrorBoundary'; +import { BottomLink, BottomLinkProps } from '../BottomLink'; +import { ErrorBoundary } from '../ErrorBoundary'; const useTabsStyles = makeStyles(theme => ({ root: { diff --git a/packages/core/src/layout/TabbedCard/index.ts b/packages/core/src/layout/TabbedCard/index.ts index f5019f2451..88d7782d55 100644 --- a/packages/core/src/layout/TabbedCard/index.ts +++ b/packages/core/src/layout/TabbedCard/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export * from './TabbedCard'; +export { CardTab, TabbedCard } from './TabbedCard'; diff --git a/packages/core/src/hooks/useSidebarPinState.ts b/packages/core/src/layout/index.ts similarity index 64% rename from packages/core/src/hooks/useSidebarPinState.ts rename to packages/core/src/layout/index.ts index bbe548c699..c9dbae9ca2 100644 --- a/packages/core/src/hooks/useSidebarPinState.ts +++ b/packages/core/src/layout/index.ts @@ -13,16 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useContext } from 'react'; -import { SidebarPinStateContext } from '../layout/Sidebar'; -export function useSidebarPinState() { - const { isPinned, toggleSidebarPinState } = useContext( - SidebarPinStateContext, - ); - - return { - isPinned, - toggleSidebarPinState, - }; -} +export * from './Content'; +export * from './ContentHeader'; +export * from './ErrorBoundary'; +export * from './Header'; +export * from './HeaderLabel'; +export * from './HomepageTimer'; +export * from './InfoCard'; +export * from './LoginPage'; +export * from './Page'; +export * from './Sidebar'; +export * from './TabbedCard'; diff --git a/packages/storybook/.storybook/apis.js b/packages/storybook/.storybook/apis.js new file mode 100644 index 0000000000..5743dac50b --- /dev/null +++ b/packages/storybook/.storybook/apis.js @@ -0,0 +1,16 @@ +import { + ApiRegistry, + alertApiRef, + errorApiRef, + AlertApiForwarder, + ErrorApiForwarder, + ErrorAlerter, +} from '@backstage/core'; + +const builder = ApiRegistry.builder(); + +const alertApi = builder.add(alertApiRef, new AlertApiForwarder()); + +builder.add(errorApiRef, new ErrorAlerter(alertApi, new ErrorApiForwarder())); + +export const apis = builder.build(); diff --git a/packages/storybook/.storybook/config.js b/packages/storybook/.storybook/config.js index 941b02718b..69145ea057 100644 --- a/packages/storybook/.storybook/config.js +++ b/packages/storybook/.storybook/config.js @@ -3,14 +3,18 @@ import { addDecorator, addParameters } from '@storybook/react'; import { lightTheme, darkTheme } from '@backstage/theme'; import { CssBaseline, ThemeProvider } from '@material-ui/core'; import { useDarkMode } from 'storybook-dark-mode'; -import { Content } from '@backstage/core'; +import { Content, ApiProvider, AlertDisplay } from '@backstage/core'; +import { apis } from './apis'; -addDecorator((story) => ( - - - {story()} - - +addDecorator(story => ( + + + + + {story()} + + + )); addParameters({ diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index fe82e544e7..9cb8017f15 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -27,14 +27,10 @@ "yn": "^4.0.0", "passport": "^0.4.1", "passport-google-oauth20": "^2.0.0", - "passport-oauth2-refresh": "^2.0.0", - "passport-oauth2": "^1.5.0", "cookie-parser": "^1.4.5", - "@types/passport-oauth2-refresh": "^1.1.1", "@types/passport": "^1.0.3", "@types/passport-google-oauth20": "^2.0.3", - "@types/cookie-parser": "^1.4.2", - "@types/passport-oauth2": "^1.4.9" + "@types/cookie-parser": "^1.4.2" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.6", diff --git a/plugins/auth-backend/src/providers/OAuthProvider.ts b/plugins/auth-backend/src/providers/OAuthProvider.ts new file mode 100644 index 0000000000..81f5ed25a6 --- /dev/null +++ b/plugins/auth-backend/src/providers/OAuthProvider.ts @@ -0,0 +1,193 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 express, { CookieOptions } from 'express'; +import crypto from 'crypto'; +import { AuthProviderRouteHandlers, OAuthProviderHandlers } from './types'; +import { InputError } from '@backstage/backend-common'; +import { postMessageResponse, ensuresXRequestedWith } from './utils'; + +export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; +export const TEN_MINUTES_MS = 600 * 1000; + +export const verifyNonce = (req: express.Request, provider: string) => { + const cookieNonce = req.cookies[`${provider}-nonce`]; + const stateNonce = req.query.state; + + if (!cookieNonce || !stateNonce) { + throw new Error('Missing nonce'); + } + + if (cookieNonce !== stateNonce) { + throw new Error('Invalid nonce'); + } +}; + +export const setNonceCookie = (res: express.Response, provider: string) => { + const nonce = crypto.randomBytes(16).toString('base64'); + + const options: CookieOptions = { + maxAge: TEN_MINUTES_MS, + secure: false, + sameSite: 'none', + domain: 'localhost', + path: `/auth/${provider}/handler`, + httpOnly: true, + }; + + res.cookie(`${provider}-nonce`, nonce, options); + + return nonce; +}; + +export const setRefreshTokenCookie = ( + res: express.Response, + provider: string, + refreshToken: string, +) => { + const options: CookieOptions = { + maxAge: THOUSAND_DAYS_MS, + secure: false, + sameSite: 'none', + domain: 'localhost', + path: `/auth/${provider}`, + httpOnly: true, + }; + + res.cookie(`${provider}-refresh-token`, refreshToken, options); +}; + +export const removeRefreshTokenCookie = ( + res: express.Response, + provider: string, +) => { + const options: CookieOptions = { + maxAge: 0, + secure: false, + sameSite: 'none', + domain: 'localhost', + path: `/auth/${provider}`, + httpOnly: true, + }; + + res.cookie(`${provider}-refresh-token`, '', options); +}; + +export class OAuthProvider implements AuthProviderRouteHandlers { + private readonly provider: string; + private readonly providerHandlers: OAuthProviderHandlers; + constructor(providerHandlers: OAuthProviderHandlers, provider: string) { + this.provider = provider; + this.providerHandlers = providerHandlers; + } + + async start(req: express.Request, res: express.Response): Promise { + // retrieve scopes from request + const scope = req.query.scope?.toString() ?? ''; + + if (!scope) { + throw new InputError('missing scope parameter'); + } + + // set a nonce cookie before redirecting to oauth provider + const nonce = setNonceCookie(res, this.provider); + + const options = { + scope, + accessType: 'offline', + prompt: 'consent', + state: nonce, + }; + const { url, status } = await this.providerHandlers.start(req, options); + + res.statusCode = status || 302; + res.setHeader('Location', url); + res.setHeader('Content-Length', '0'); + res.end(); + } + + async frameHandler( + req: express.Request, + res: express.Response, + ): Promise { + try { + // verify nonce cookie and state cookie on callback + verifyNonce(req, this.provider); + + const { user, info } = await this.providerHandlers.handler(req); + + // throw error if missing refresh token + const { refreshToken } = info; + if (!refreshToken) { + throw new Error('Missing refresh token'); + } + + // set new refresh token + setRefreshTokenCookie(res, this.provider, refreshToken); + + // post message back to popup if successful + return postMessageResponse(res, { + type: 'auth-result', + payload: user, + }); + } catch (error) { + // post error message back to popup if failure + return postMessageResponse(res, { + type: 'auth-result', + error: { + name: error.name, + message: error.message, + }, + }); + } + } + + async logout(req: express.Request, res: express.Response): Promise { + if (!ensuresXRequestedWith(req)) { + return res.status(401).send('Invalid X-Requested-With header'); + } + + // remove refresh token cookie before logout + removeRefreshTokenCookie(res, this.provider); + return res.send('logout!'); + } + + async refresh(req: express.Request, res: express.Response): Promise { + if (!ensuresXRequestedWith(req)) { + return res.status(401).send('Invalid X-Requested-With header'); + } + + try { + const refreshToken = req.cookies[`${this.provider}-refresh-token`]; + + // throw error if refresh token is missing in the request + if (!refreshToken) { + throw new Error('Missing session cookie'); + } + + const scope = req.query.scope?.toString() ?? ''; + + // get new access_token + const refreshInfo = await this.providerHandlers.refresh( + refreshToken, + scope, + ); + return res.send(refreshInfo); + } catch (error) { + return res.status(401).send(`${error.message}`); + } + } +} diff --git a/plugins/auth-backend/src/providers/PassportStrategyHelper.ts b/plugins/auth-backend/src/providers/PassportStrategyHelper.ts new file mode 100644 index 0000000000..3ec8539330 --- /dev/null +++ b/plugins/auth-backend/src/providers/PassportStrategyHelper.ts @@ -0,0 +1,108 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 express from 'express'; +import passport from 'passport'; +import { RedirectInfo, RefreshTokenResponse } from './types'; + +export const executeRedirectStrategy = async ( + req: express.Request, + providerStrategy: passport.Strategy, + options: any, +): Promise => { + return new Promise(resolve => { + const strategy = Object.create(providerStrategy); + strategy.redirect = (url: string, status?: number) => { + resolve({ url, status: status ?? undefined }); + }; + + strategy.authenticate(req, { ...options }); + }); +}; + +export const executeFrameHandlerStrategy = async ( + req: express.Request, + providerStrategy: passport.Strategy, +) => { + return new Promise<{ user: any; info: any }>((resolve, reject) => { + const strategy = Object.create(providerStrategy); + strategy.success = (user: any, info: any) => { + resolve({ user, info }); + }; + strategy.fail = ( + info: { type: 'success' | 'error'; message?: string }, + // _status: number, + ) => { + reject(new Error(`Authentication rejected, ${info.message ?? ''}`)); + }; + strategy.error = (error: Error) => { + reject(new Error(`Authentication failed, ${error}`)); + }; + strategy.redirect = () => { + reject(new Error('Unexpected redirect')); + }; + + strategy.authenticate(req); + }); +}; + +export const executeRefreshTokenStrategy = async ( + providerstrategy: passport.Strategy, + refreshToken: string, + scope: string, +): Promise => { + return new Promise((resolve, reject) => { + const anyStrategy = providerstrategy as any; + const OAuth2 = anyStrategy._oauth2.constructor; + const oauth2 = new OAuth2( + anyStrategy._oauth2._clientId, + anyStrategy._oauth2._clientSecret, + anyStrategy._oauth2._baseSite, + anyStrategy._oauth2._authorizeUrl, + anyStrategy._refreshURL || anyStrategy._oauth2._accessTokenUrl, + anyStrategy._oauth2._customHeaders, + ); + + oauth2.getOAuthAccessToken( + refreshToken, + { + scope, + grant_type: 'refresh_token', + }, + ( + err: Error | null, + accessToken: string, + _refreshToken: string, + params: any, + ) => { + if (err) { + reject(new Error(`Failed to refresh access token ${err}`)); + } + if (!accessToken) { + reject( + new Error( + `Failed to refresh access token, no access token received`, + ), + ); + } + resolve({ + accessToken, + params, + }); + }, + ); + }); +}; diff --git a/plugins/auth-backend/src/providers/factories.test.ts b/plugins/auth-backend/src/providers/factories.test.ts deleted file mode 100644 index 1647f62682..0000000000 --- a/plugins/auth-backend/src/providers/factories.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * 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 express from 'express'; -import passport from 'passport'; -import { AuthProvider, AuthProviderRouteHandlers } from './types'; -import { ProviderFactories } from './factories'; - -class MyAuthProvider implements AuthProvider, AuthProviderRouteHandlers { - strategy(): passport.Strategy { - return new passport.Strategy(); - } - async start(_: express.Request, res: express.Response): Promise { - res.send('start'); - } - async frameHandler(_: express.Request, res: express.Response): Promise { - res.send('frameHandler'); - } - async logout(_: express.Request, res: express.Response): Promise { - res.send('logout'); - } -} - -describe('getProviderFactory', () => { - it('makes a provider for MyAuthProvider', () => { - jest - .spyOn(ProviderFactories, 'getProviderFactory') - .mockReturnValueOnce(MyAuthProvider); - const provider = ProviderFactories.getProviderFactory('a'); - expect(provider).toBeDefined(); - }); - - it('throws an error when provider implementation does not exist', () => { - expect(() => { - ProviderFactories.getProviderFactory('b'); - }).toThrow('Provider Implementation missing for : b auth provider'); - }); -}); diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 077d45076e..0a1e639082 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -15,7 +15,7 @@ */ import { AuthProviderFactories, AuthProviderFactory } from './types'; -import { GoogleAuthProvider } from './google/provider'; +import { GoogleAuthProvider } from './google'; export class ProviderFactories { private static readonly providerFactories: AuthProviderFactories = { diff --git a/packages/core/src/icons/types.ts b/plugins/auth-backend/src/providers/google/index.ts similarity index 69% rename from packages/core/src/icons/types.ts rename to plugins/auth-backend/src/providers/google/index.ts index 599cb969df..0ec98bef89 100644 --- a/packages/core/src/icons/types.ts +++ b/plugins/auth-backend/src/providers/google/index.ts @@ -14,8 +14,4 @@ * limitations under the License. */ -import { ComponentType } from 'react'; -import { SvgIconProps } from '@material-ui/core'; -export type IconComponent = ComponentType; -export type SystemIconKey = 'user' | 'group'; -export type SystemIcons = { [key in SystemIconKey]: IconComponent }; +export { GoogleAuthProvider } from './provider'; diff --git a/plugins/auth-backend/src/providers/google/provider.test.ts b/plugins/auth-backend/src/providers/google/provider.test.ts deleted file mode 100644 index 327bf0260b..0000000000 --- a/plugins/auth-backend/src/providers/google/provider.test.ts +++ /dev/null @@ -1,522 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * 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 { - GoogleAuthProvider, - THOUSAND_DAYS_MS, - TEN_MINUTES_MS, -} from './provider'; -import passport from 'passport'; -import express from 'express'; -import * as utils from './../utils'; -import refresh from 'passport-oauth2-refresh'; - -const googleAuthProviderConfig = { - provider: 'google', - options: { - clientID: 'a', - clientSecret: 'b', - callbackURL: 'c', - }, -}; - -const googleAuthProviderConfigInvalidOptions = { - provider: 'google', - options: {}, -}; - -describe('GoogleAuthProvider', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - describe('create a new provider', () => { - it('should succeed with valid config', () => { - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); - expect(googleAuthProvider).toBeDefined(); - expect(googleAuthProvider.start).toBeDefined(); - expect(googleAuthProvider.logout).toBeDefined(); - expect(googleAuthProvider.frameHandler).toBeDefined(); - expect(googleAuthProvider.strategy).toBeDefined(); - }); - }); - - describe('start authentication handler', () => { - const mockResponse = ({ - send: jest.fn().mockReturnThis(), - status: jest.fn().mockReturnThis(), - cookie: jest.fn().mockReturnThis(), - } as unknown) as express.Response; - const mockNext: express.NextFunction = jest.fn(); - - it('should initiate authenticate request with provided scopes', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - query: { - scope: 'a,b', - }, - } as unknown) as express.Request; - - const spyPassport = jest - .spyOn(passport, 'authenticate') - .mockImplementation(() => jest.fn()); - - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); - googleAuthProvider.start(mockRequest, mockResponse, mockNext); - expect(spyPassport).toBeCalledTimes(1); - expect(spyPassport).toBeCalledWith('google', { - scope: 'a,b', - accessType: 'offline', - prompt: 'consent', - state: expect.any(String), - }); - }); - - it('should set a nonce cookie', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - query: { - scope: 'a,b', - }, - } as unknown) as express.Request; - - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); - googleAuthProvider.start(mockRequest, mockResponse, mockNext); - expect(mockResponse.cookie).toBeCalledTimes(1); - expect(mockResponse.cookie).toBeCalledWith( - 'google-nonce', - expect.any(String), - expect.objectContaining({ - maxAge: TEN_MINUTES_MS, - path: `/auth/${googleAuthProviderConfig.provider}/handler`, - }), - ); - }); - - it('should throw error if no scopes provided', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - query: {}, - } as unknown) as express.Request; - - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); - expect(() => { - googleAuthProvider.start(mockRequest, mockResponse, mockNext); - }).toThrowError('missing scope parameter'); - }); - }); - - describe('logout handler', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - } as unknown) as express.Request; - - it('should perform logout and respond with 200', () => { - const mockResponse: any = ({ - send: jest.fn(), - cookie: jest.fn(), - } as unknown) as express.Response; - - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); - - const spyResponse = jest - .spyOn(mockResponse, 'send') - .mockImplementation(() => jest.fn()); - - googleAuthProvider.logout(mockRequest, mockResponse); - expect(spyResponse).toBeCalledTimes(1); - expect(spyResponse).toBeCalledWith('logout!'); - expect(mockResponse.cookie).toBeCalledTimes(1); - expect(mockResponse.cookie).toBeCalledWith( - 'google-refresh-token', - '', - expect.objectContaining({ maxAge: 0 }), - ); - }); - }); - - describe('redirect frame handler', () => { - const mockResponse: any = ({ - status: jest.fn().mockReturnThis(), - send: jest.fn().mockReturnThis(), - cookie: jest.fn().mockReturnThis(), - } as unknown) as express.Response; - const mockNext: express.NextFunction = jest.fn(); - - it('should call authenticate and post a response', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - cookies: { 'google-nonce': 'NONCE' }, - query: { - state: 'NONCE', - }, - } as unknown) as express.Request; - - const spyPostMessage = jest - .spyOn(utils, 'postMessageResponse') - .mockImplementation(() => jest.fn()); - - const spyPassport = jest - .spyOn(passport, 'authenticate') - .mockImplementation((_x, callbackFunc) => { - const cb = callbackFunc as Function; - cb(null, { refreshToken: 'REFRESH_TOKEN' }); - return jest.fn(); - }); - - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); - - googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext); - expect(spyPassport).toBeCalledTimes(1); - expect(spyPostMessage).toBeCalledTimes(1); - expect(mockResponse.cookie).toBeCalledTimes(1); - expect(mockResponse.cookie).toBeCalledWith( - 'google-refresh-token', - 'REFRESH_TOKEN', - expect.objectContaining({ - path: '/auth/google', - sameSite: 'none', - httpOnly: true, - maxAge: THOUSAND_DAYS_MS, - }), - ); - }); - - it('should respond with a error message if no refresh token returned', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - cookies: { 'google-nonce': 'NONCE' }, - query: { - state: 'NONCE', - }, - } as unknown) as express.Request; - - const spyPassport = jest - .spyOn(passport, 'authenticate') - .mockImplementation((_x, callbackFunc) => { - const cb = callbackFunc as Function; - cb(null, {}); - return jest.fn(); - }); - - const spyPostMessage = jest - .spyOn(utils, 'postMessageResponse') - .mockImplementation(() => jest.fn()); - - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); - - googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext); - expect(spyPassport).toBeCalledTimes(1); - expect(spyPostMessage).toBeCalledTimes(1); - expect(spyPostMessage).toBeCalledWith(mockResponse, { - type: 'auth-result', - error: new Error('Missing refresh token'), - }); - }); - - it('should respond with a error message if auth failed', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - cookies: { 'google-nonce': 'NONCE' }, - query: { - state: 'NONCE', - }, - } as unknown) as express.Request; - - const spyPassport = jest - .spyOn(passport, 'authenticate') - .mockImplementation((_x, callbackFunc) => { - const cb = callbackFunc as Function; - cb(new Error('TokenError'), null); - return jest.fn(); - }); - - const spyPostMessage = jest - .spyOn(utils, 'postMessageResponse') - .mockImplementation(() => jest.fn()); - - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); - - googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext); - expect(spyPassport).toBeCalledTimes(1); - expect(spyPostMessage).toBeCalledTimes(1); - expect(spyPostMessage).toBeCalledWith(mockResponse, { - type: 'auth-result', - error: new Error('Google auth failed, Error: TokenError'), - }); - }); - - it('should respond with a error message if cookie nonce is missing', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - cookies: {}, - query: { state: 'NONCE' }, - } as unknown) as express.Request; - - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); - - googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext); - expect(mockResponse.send).toBeCalledTimes(1); - expect(mockResponse.send).toBeCalledWith('Missing nonce'); - expect(mockResponse.status).toBeCalledTimes(1); - expect(mockResponse.status).toBeCalledWith(401); - }); - - it('should respond with a error message if state nonce is missing', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - cookies: { 'google-nonce': 'NONCE' }, - query: {}, - } as unknown) as express.Request; - - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); - - googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext); - expect(mockResponse.send).toBeCalledTimes(1); - expect(mockResponse.send).toBeCalledWith('Missing nonce'); - expect(mockResponse.status).toBeCalledTimes(1); - expect(mockResponse.status).toBeCalledWith(401); - }); - - it('should respond with a error message if nonce mismatch', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - cookies: { 'google-nonce': 'NONCA' }, - query: { state: 'NONCEB' }, - } as unknown) as express.Request; - - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); - - googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext); - expect(mockResponse.send).toBeCalledTimes(1); - expect(mockResponse.send).toBeCalledWith('Invalid nonce'); - expect(mockResponse.status).toBeCalledTimes(1); - expect(mockResponse.status).toBeCalledWith(401); - }); - }); - - describe('strategy handler', () => { - it('should return a valid passport strategy', () => { - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); - - expect(googleAuthProvider.strategy()).toBeInstanceOf(passport.Strategy); - }); - - it('should throw an error for invalid options', () => { - expect(() => { - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfigInvalidOptions, - ); - googleAuthProvider.strategy(); - }).toThrow(); - }); - }); - - describe('refresh token handler', () => { - const mockResponse = ({ - status: jest.fn().mockReturnThis(), - send: jest.fn().mockReturnThis(), - } as unknown) as express.Response; - - describe('no refresh token cookie', () => { - it('should respond with a 401', () => { - const mockRequest = ({ - cookies: jest.fn(), - header: () => 'XMLHttpRequest', - } as unknown) as express.Request; - - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); - - googleAuthProvider.refresh(mockRequest, mockResponse); - expect(mockResponse.send).toBeCalledTimes(1); - expect(mockResponse.send).toBeCalledWith('Missing session cookie'); - - expect(mockResponse.status).toBeCalledTimes(1); - expect(mockResponse.status).toBeCalledWith(401); - }); - }); - - describe('refresh token cookie, no scope', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - cookies: { 'google-refresh-token': 'REFRESH_TOKEN' }, - query: {}, - } as unknown) as express.Request; - - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); - - it('should request for a new access token and fail if no access token returned', () => { - const spyRefresh = jest - .spyOn(refresh, 'requestNewAccessToken') - .mockImplementation((_x, _y, _z, callbackFunc) => { - const cb = callbackFunc as Function; - cb(undefined, undefined, undefined, {}); - }); - - googleAuthProvider.refresh(mockRequest, mockResponse); - expect(spyRefresh).toBeCalledTimes(1); - expect(spyRefresh).toBeCalledWith( - 'google', - 'REFRESH_TOKEN', - {}, - expect.any(Function), - ); - expect(mockResponse.status).toBeCalledTimes(1); - expect(mockResponse.status).toBeCalledWith(401); - expect(mockResponse.send).toBeCalledTimes(1); - expect(mockResponse.send).toBeCalledWith( - 'Failed to refresh access token', - ); - }); - - it('should request for a new access token and return 401 if any error', () => { - const spyRefresh = jest - .spyOn(refresh, 'requestNewAccessToken') - .mockImplementation((_x, _y, _z, callbackFunc) => { - const cb = callbackFunc as Function; - cb({ error: 'ERROR' }, undefined, undefined, {}); - }); - - googleAuthProvider.refresh(mockRequest, mockResponse); - expect(spyRefresh).toBeCalledTimes(1); - expect(spyRefresh).toBeCalledWith( - 'google', - 'REFRESH_TOKEN', - {}, - expect.any(Function), - ); - expect(mockResponse.status).toBeCalledTimes(1); - expect(mockResponse.status).toBeCalledWith(401); - expect(mockResponse.send).toBeCalledTimes(1); - expect(mockResponse.send).toBeCalledWith( - 'Failed to refresh access token', - ); - }); - - it('should fetch and return a new access token', () => { - const spyRefresh = jest - .spyOn(refresh, 'requestNewAccessToken') - .mockImplementation((_x, _y, _z, callbackFunc) => { - const cb = callbackFunc as Function; - cb(undefined, 'ACCESS_TOKEN', undefined, { - expires_in: 'EXPIRES_IN', - id_token: 'ID_TOKEN', - }); - }); - - googleAuthProvider.refresh(mockRequest, mockResponse); - expect(spyRefresh).toBeCalledTimes(1); - expect(spyRefresh).toBeCalledWith( - 'google', - 'REFRESH_TOKEN', - {}, - expect.any(Function), - ); - expect(mockResponse.send).toBeCalledTimes(1); - expect(mockResponse.send).toBeCalledWith({ - accessToken: 'ACCESS_TOKEN', - idToken: 'ID_TOKEN', - expiresInSeconds: 'EXPIRES_IN', - scope: undefined, - }); - }); - }); - - describe('refresh token cookie and scope', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - cookies: { 'google-refresh-token': 'REFRESH_TOKEN' }, - query: { - scope: 'a,b', - }, - } as unknown) as express.Request; - - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); - - it('should fetch and return a new access token with scopes', () => { - const spyRefresh = jest - .spyOn(refresh, 'requestNewAccessToken') - .mockImplementation((_x, _y, _z, callbackFunc) => { - const cb = callbackFunc as Function; - cb(undefined, 'ACCESS_TOKEN', undefined, { - expires_in: 'EXPIRES_IN', - id_token: 'ID_TOKEN', - scope: 'a,b', - }); - }); - - googleAuthProvider.refresh(mockRequest, mockResponse); - expect(spyRefresh).toBeCalledTimes(1); - expect(spyRefresh).toBeCalledWith( - 'google', - 'REFRESH_TOKEN', - { scope: 'a,b' }, - expect.any(Function), - ); - expect(mockResponse.send).toBeCalledTimes(1); - expect(mockResponse.send).toBeCalledWith({ - accessToken: 'ACCESS_TOKEN', - idToken: 'ID_TOKEN', - expiresInSeconds: 'EXPIRES_IN', - scope: 'a,b', - }); - }); - - it('ensures x-requested-with header', () => { - const mockHeaderRequest = ({ - header: () => 'TEST', - } as unknown) as express.Request; - - googleAuthProvider.refresh(mockHeaderRequest, mockResponse); - expect(mockResponse.send).toBeCalledTimes(1); - expect(mockResponse.send).toBeCalledWith( - 'Invalid X-Requested-With header', - ); - expect(mockResponse.status).toBeCalledTimes(1); - expect(mockResponse.status).toBeCalledWith(401); - }); - }); - }); -}); diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index cb080e2fd3..90d33652a5 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -14,168 +14,29 @@ * limitations under the License. */ -import passport from 'passport'; -import express, { CookieOptions } from 'express'; -import crypto from 'crypto'; +import express from 'express'; import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; -import refresh from 'passport-oauth2-refresh'; import { - AuthProvider, - AuthProviderRouteHandlers, + executeFrameHandlerStrategy, + executeRedirectStrategy, + executeRefreshTokenStrategy, +} from '../PassportStrategyHelper'; +import { + OAuthProviderHandlers, + AuthInfoBase, + AuthInfoPrivate, + RedirectInfo, AuthProviderConfig, -} from './../types'; -import { postMessageResponse, ensuresXRequestedWith } from './../utils'; -import { InputError } from '@backstage/backend-common'; +} from '../types'; -export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; -export const TEN_MINUTES_MS = 600 * 1000; -export class GoogleAuthProvider - implements AuthProvider, AuthProviderRouteHandlers { +export class GoogleAuthProvider implements OAuthProviderHandlers { private readonly providerConfig: AuthProviderConfig; + private readonly _strategy: GoogleStrategy; + constructor(providerConfig: AuthProviderConfig) { this.providerConfig = providerConfig; - } - - start( - req: express.Request, - res: express.Response, - next: express.NextFunction, - ) { - const nonce = crypto.randomBytes(16).toString('base64'); - - const options: CookieOptions = { - maxAge: TEN_MINUTES_MS, - secure: false, - sameSite: 'none', - domain: 'localhost', - path: `/auth/${this.providerConfig.provider}/handler`, - httpOnly: true, - }; - - res.cookie(`${this.providerConfig.provider}-nonce`, nonce, options); - - const scope = req.query.scope?.toString() ?? ''; - if (!scope) { - throw new InputError('missing scope parameter'); - } - return passport.authenticate('google', { - scope, - accessType: 'offline', - prompt: 'consent', - state: nonce, - })(req, res, next); - } - - frameHandler( - req: express.Request, - res: express.Response, - next: express.NextFunction, - ) { - const cookieNonce = req.cookies[`${this.providerConfig.provider}-nonce`]; - const stateNonce = req.query.state; - - if (!cookieNonce || !stateNonce) { - return res.status(401).send('Missing nonce'); - } - - if (cookieNonce !== stateNonce) { - return res.status(401).send('Invalid nonce'); - } - - return passport.authenticate('google', (err, user) => { - if (err) { - return postMessageResponse(res, { - type: 'auth-result', - error: new Error(`Google auth failed, ${err}`), - }); - } - - const { refreshToken } = user; - - if (!refreshToken) { - return postMessageResponse(res, { - type: 'auth-result', - error: new Error('Missing refresh token'), - }); - } - - delete user.refreshToken; - - const options: CookieOptions = { - maxAge: THOUSAND_DAYS_MS, - secure: false, - sameSite: 'none', - domain: 'localhost', - path: `/auth/${this.providerConfig.provider}`, - httpOnly: true, - }; - - res.cookie( - `${this.providerConfig.provider}-refresh-token`, - refreshToken, - options, - ); - return postMessageResponse(res, { - type: 'auth-result', - payload: user, - }); - })(req, res, next); - } - - async logout(req: express.Request, res: express.Response) { - if (!ensuresXRequestedWith(req)) { - return res.status(401).send('Invalid X-Requested-With header'); - } - - const options: CookieOptions = { - maxAge: 0, - secure: false, - sameSite: 'none', - domain: 'localhost', - path: `/auth/${this.providerConfig.provider}`, - httpOnly: true, - }; - - res.cookie(`${this.providerConfig.provider}-refresh-token`, '', options); - return res.send('logout!'); - } - - async refresh(req: express.Request, res: express.Response) { - if (!ensuresXRequestedWith(req)) { - return res.status(401).send('Invalid X-Requested-With header'); - } - - const refreshToken = - req.cookies[`${this.providerConfig.provider}-refresh-token`]; - - if (!refreshToken) { - return res.status(401).send('Missing session cookie'); - } - - const scope = req.query.scope?.toString() ?? ''; - const refreshTokenRequestParams = scope ? { scope } : {}; - - return refresh.requestNewAccessToken( - this.providerConfig.provider, - refreshToken, - refreshTokenRequestParams, - (err, accessToken, _refreshToken, params) => { - if (err || !accessToken) { - return res.status(401).send('Failed to refresh access token'); - } - return res.send({ - accessToken, - idToken: params.id_token, - expiresInSeconds: params.expires_in, - scope: params.scope, - }); - }, - ); - } - - strategy(): passport.Strategy { // TODO: throw error if env variables not set? - return new GoogleStrategy( + this._strategy = new GoogleStrategy( { ...this.providerConfig.options }, ( accessToken: any, @@ -184,15 +45,45 @@ export class GoogleAuthProvider profile: any, done: any, ) => { - done(undefined, { - profile, - idToken: params.id_token, - accessToken, - refreshToken, - scope: params.scope, - expiresInSeconds: params.expires_in, - }); + done( + undefined, + { + profile, + idToken: params.id_token, + accessToken, + scope: params.scope, + expiresInSeconds: params.expires_in, + }, + { + refreshToken, + }, + ); }, ); } + + async start(req: express.Request, options: any): Promise { + return await executeRedirectStrategy(req, this._strategy, options); + } + + async handler( + req: express.Request, + ): Promise<{ user: AuthInfoBase; info: AuthInfoPrivate }> { + return await executeFrameHandlerStrategy(req, this._strategy); + } + + async refresh(refreshToken: string, scope: string): Promise { + const { accessToken, params } = await executeRefreshTokenStrategy( + this._strategy, + refreshToken, + scope, + ); + + return { + accessToken, + idToken: params.id_token, + expiresInSeconds: params.expires_in, + scope: params.scope, + }; + } } diff --git a/plugins/auth-backend/src/providers/index.test.ts b/plugins/auth-backend/src/providers/index.test.ts index e42cd32edc..b3e2f19771 100644 --- a/plugins/auth-backend/src/providers/index.test.ts +++ b/plugins/auth-backend/src/providers/index.test.ts @@ -14,90 +14,8 @@ * limitations under the License. */ -import passport from 'passport'; -import express from 'express'; -import { makeProvider, defaultRouter } from '.'; -import { - AuthProvider, - AuthProviderRouteHandlers, - AuthProviderConfig, -} from './types'; -import * as passportGoogleOAuth20 from 'passport-google-oauth20'; -import { ProviderFactories } from './factories'; - -class MyAuthProvider implements AuthProvider, AuthProviderRouteHandlers { - private readonly providerConfig: AuthProviderConfig; - constructor(providerConfig: AuthProviderConfig) { - this.providerConfig = providerConfig; - } - - strategy(): passport.Strategy { - return new passportGoogleOAuth20.Strategy( - this.providerConfig.options, - () => {}, - ); - } - async start(_: express.Request, res: express.Response): Promise { - res.send('start'); - } - async frameHandler(_: express.Request, res: express.Response): Promise { - res.send('frameHandler'); - } - async logout(_: express.Request, res: express.Response): Promise { - res.send('logout'); - } -} - -class MyAuthProviderWithRefresh extends MyAuthProvider { - async refresh(_: express.Request, res: express.Response): Promise { - res.send('logout'); - } -} - -const providerConfig = { - provider: 'a', - options: { - clientID: 'somevalue', - }, -}; - -const providerConfigInvalid = { - provider: 'b', - options: { - clientID: 'somevalue', - }, -}; - -describe('makeProvider', () => { - it('makes a provider for Myauthprovider', () => { - jest - .spyOn(ProviderFactories, 'getProviderFactory') - .mockReturnValueOnce(MyAuthProvider); - const provider = makeProvider(providerConfig); - expect(provider.providerId).toEqual('a'); - expect(provider.strategy).toBeDefined(); - expect(provider.providerRouter).toBeDefined(); - }); - - it('throws an error when provider implementation does not exist', () => { - expect(() => { - makeProvider(providerConfigInvalid); - }).toThrow('Provider Implementation missing for : b auth provider'); - }); -}); - -describe('defaultRouter', () => { - it('make router for auth provider without refresh', () => { - expect( - defaultRouter(new MyAuthProvider({ provider: 'a', options: {} })), - ).toBeDefined(); - }); - - it('make router for auth provider with refresh', () => { - expect( - defaultRouter( - new MyAuthProviderWithRefresh({ provider: 'b', options: {} }), - ), - ).toBeDefined(); +describe('test', () => { + it('unbreaks the test runner', () => { + expect(true).toBeTruthy(); }); }); diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 1b33391c27..59dd116fa6 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -17,6 +17,7 @@ import Router from 'express-promise-router'; import { AuthProviderRouteHandlers, AuthProviderConfig } from './types'; import { ProviderFactories } from './factories'; +import { OAuthProvider } from './OAuthProvider'; export const defaultRouter = (provider: AuthProviderRouteHandlers) => { const router = Router(); @@ -33,7 +34,8 @@ export const makeProvider = (config: AuthProviderConfig) => { const providerId = config.provider; const ProviderImpl = ProviderFactories.getProviderFactory(providerId); const providerInstance = new ProviderImpl(config); - const strategy = providerInstance.strategy(); - const providerRouter = defaultRouter(providerInstance); - return { providerId, strategy, providerRouter }; + + const oauthProvider = new OAuthProvider(providerInstance, providerId); + const providerRouter = defaultRouter(oauthProvider); + return { providerId, providerRouter }; }; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 36941c6850..dcbe6ad1de 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -22,32 +22,18 @@ export type AuthProviderConfig = { options: any; }; -export interface AuthProvider { - strategy(): passport.Strategy; - router?(): express.Router; +export interface OAuthProviderHandlers { + start(req: express.Request, options: any): Promise; + handler(req: express.Request): Promise; + refresh(refreshToken: string, scope: string): Promise; + logout?(): Promise; } export interface AuthProviderRouteHandlers { - start( - req: express.Request, - res: express.Response, - next: express.NextFunction, - ): Promise; - frameHandler( - req: express.Request, - res: express.Response, - next: express.NextFunction, - ): Promise; - refresh?( - req: express.Request, - res: express.Response, - next: express.NextFunction, - ): Promise; - logout( - req: express.Request, - res: express.Response, - next: express.NextFunction, - ): Promise; + start(req: express.Request, res: express.Response): Promise; + frameHandler(req: express.Request, res: express.Response): Promise; + refresh?(req: express.Request, res: express.Response): Promise; + logout(req: express.Request, res: express.Response): Promise; } export type AuthProviderFactories = { @@ -55,7 +41,7 @@ export type AuthProviderFactories = { }; export type AuthProviderFactory = { - new (providerConfig: any): AuthProvider & AuthProviderRouteHandlers; + new (providerConfig: any): OAuthProviderHandlers; }; export type AuthInfoBase = { @@ -69,7 +55,7 @@ export type AuthInfoWithProfile = AuthInfoBase & { profile: passport.Profile; }; -export type AuthInfoPrivate = AuthInfoWithProfile & { +export type AuthInfoPrivate = { refreshToken: string; }; @@ -82,3 +68,13 @@ export type AuthResponse = type: 'auth-result'; error: Error; }; + +export type RedirectInfo = { + url: string; + status?: number; +}; + +export type RefreshTokenResponse = { + accessToken: string; + params: any; +}; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index a2e3b3e1db..49487d551a 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -16,10 +16,7 @@ import express from 'express'; import Router from 'express-promise-router'; -import passport from 'passport'; import cookieParser from 'cookie-parser'; -import refresh from 'passport-oauth2-refresh'; -import OAuth2Strategy from 'passport-oauth2'; import { Logger } from 'winston'; import { providers } from './../providers/config'; import { makeProvider } from '../providers'; @@ -33,38 +30,14 @@ export async function createRouter( ): Promise { const router = Router(); const logger = options.logger.child({ plugin: 'auth' }); - const providerRouters: { [key: string]: express.Router } = {}; + + router.use(cookieParser()); // configure all the providers for (const providerConfig of providers) { - const { providerId, strategy, providerRouter } = makeProvider( - providerConfig, - ); - logger.info(`Configuring provider: ${providerId}`); - passport.use(strategy); - if (strategy instanceof OAuth2Strategy) { - refresh.use(strategy); - } - providerRouters[providerId] = providerRouter; - } - - passport.serializeUser((user, done) => { - done(null, user); - }); - - passport.deserializeUser((user, done) => { - done(null, user); - }); - - router.use(passport.initialize()); - router.use(passport.session()); - router.use(cookieParser()); - - for (const providerId in providerRouters) { - if (providerRouters.hasOwnProperty(providerId)) { - const providerRouter = providerRouters[providerId]; - router.use(`/${providerId}`, providerRouter); - } + const { providerId, providerRouter } = makeProvider(providerConfig); + logger.info(`Configuring provider, ${providerId}`); + router.use(`/${providerId}`, providerRouter); } return router; diff --git a/plugins/auth-backend/src/setupTests.ts b/plugins/auth-backend/src/setupTests.ts index 3fa7cb04b4..a3b2b7e123 100644 --- a/plugins/auth-backend/src/setupTests.ts +++ b/plugins/auth-backend/src/setupTests.ts @@ -15,3 +15,4 @@ */ require('jest-fetch-mock').enableMocks(); +export {}; diff --git a/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts index 1de606d44d..64ac5f57d5 100644 --- a/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts @@ -31,7 +31,7 @@ export class StaticEntitiesCatalog implements EntitiesCatalog { } async entityByUid(uid: string): Promise { - const item = this._entities.find(e => uid === e.metadata?.uid); + const item = this._entities.find(e => uid === e.metadata.uid); if (!item) { throw new NotFoundError('Entity cannot be found'); } @@ -46,8 +46,8 @@ export class StaticEntitiesCatalog implements EntitiesCatalog { const item = this._entities.find( e => kind === e.kind && - name === e.metadata?.name && - namespace === e.metadata?.namespace, + name === e.metadata.name && + namespace === e.metadata.namespace, ); if (!item) { throw new NotFoundError('Entity cannot be found'); diff --git a/plugins/catalog-backend/src/database/Database.test.ts b/plugins/catalog-backend/src/database/Database.test.ts index 5081408d3f..88f282e1e5 100644 --- a/plugins/catalog-backend/src/database/Database.test.ts +++ b/plugins/catalog-backend/src/database/Database.test.ts @@ -135,7 +135,7 @@ describe('Database', () => { catalog.addEntity(tx, entityRequest), ); expect(added).toStrictEqual(entityResponse); - expect(added.entity.metadata!.generation).toBe(1); + expect(added.entity.metadata.generation).toBe(1); }); it('rejects adding the same-named entity twice', async () => { @@ -148,9 +148,9 @@ describe('Database', () => { it('accepts adding the same-named entity twice if on different namespaces', async () => { const catalog = new Database(database, getVoidLogger()); - entityRequest.entity.metadata!.namespace = 'namespace1'; + entityRequest.entity.metadata.namespace = 'namespace1'; await catalog.transaction(tx => catalog.addEntity(tx, entityRequest)); - entityRequest.entity.metadata!.namespace = 'namespace2'; + entityRequest.entity.metadata.namespace = 'namespace2'; await expect( catalog.transaction(tx => catalog.addEntity(tx, entityRequest)), ).resolves.toBeDefined(); @@ -207,17 +207,15 @@ describe('Database', () => { ); expect(updated.entity.apiVersion).toEqual(added.entity.apiVersion); expect(updated.entity.kind).toEqual(added.entity.kind); - expect(updated.entity.metadata!.etag).not.toEqual( - added.entity.metadata!.etag, + expect(updated.entity.metadata.etag).not.toEqual( + added.entity.metadata.etag, ); - expect(updated.entity.metadata!.generation).toEqual( - added.entity.metadata!.generation, + expect(updated.entity.metadata.generation).toEqual( + added.entity.metadata.generation, ); - expect(updated.entity.metadata!.name).toEqual( - added.entity.metadata!.name, - ); - expect(updated.entity.metadata!.namespace).toEqual( - added.entity.metadata!.namespace, + expect(updated.entity.metadata.name).toEqual(added.entity.metadata.name); + expect(updated.entity.metadata.namespace).toEqual( + added.entity.metadata.namespace, ); }); @@ -226,11 +224,11 @@ describe('Database', () => { const added = await catalog.transaction(tx => catalog.addEntity(tx, entityRequest), ); - added.entity.metadata!.name! = 'new!'; + added.entity.metadata.name! = 'new!'; const updated = await catalog.transaction(tx => catalog.updateEntity(tx, { entity: added.entity }), ); - expect(updated.entity.metadata!.name).toEqual('new!'); + expect(updated.entity.metadata.name).toEqual('new!'); }); it('can update fields if kind, name, and namespace match', async () => { @@ -239,8 +237,8 @@ describe('Database', () => { catalog.addEntity(tx, entityRequest), ); added.entity.apiVersion = 'something.new'; - delete added.entity.metadata!.uid; - delete added.entity.metadata!.generation; + delete added.entity.metadata.uid; + delete added.entity.metadata.generation; const updated = await catalog.transaction(tx => catalog.updateEntity(tx, { entity: added.entity }), ); @@ -253,9 +251,9 @@ describe('Database', () => { catalog.addEntity(tx, entityRequest), ); added.entity.apiVersion = 'something.new'; - delete added.entity.metadata!.uid; - delete added.entity.metadata!.generation; - added.entity.metadata!.namespace = 'something.wrong'; + delete added.entity.metadata.uid; + delete added.entity.metadata.generation; + added.entity.metadata.namespace = 'something.wrong'; await expect( catalog.transaction(tx => catalog.updateEntity(tx, { entity: added.entity }), @@ -268,7 +266,7 @@ describe('Database', () => { const added = await catalog.transaction(tx => catalog.addEntity(tx, entityRequest), ); - added.entity.metadata!.etag = 'garbage'; + added.entity.metadata.etag = 'garbage'; await expect( catalog.transaction(tx => catalog.updateEntity(tx, { entity: added.entity }), @@ -281,7 +279,7 @@ describe('Database', () => { const added = await catalog.transaction(tx => catalog.addEntity(tx, entityRequest), ); - added.entity.metadata!.generation! += 100; + added.entity.metadata.generation! += 100; await expect( catalog.transaction(tx => catalog.updateEntity(tx, { entity: added.entity }), @@ -293,10 +291,15 @@ describe('Database', () => { describe('entities', () => { it('can get all entities with empty filters list', async () => { const catalog = new Database(database, getVoidLogger()); - const e1: Entity = { apiVersion: 'a', kind: 'b' }; - const e2: Entity = { + const e1: Entity = { apiVersion: 'a', - kind: 'b', + kind: 'k1', + metadata: { name: 'n' }, + }; + const e2: Entity = { + apiVersion: 'c', + kind: 'k2', + metadata: { name: 'n' }, spec: { c: null }, }; await catalog.transaction(async tx => { @@ -309,8 +312,14 @@ describe('Database', () => { expect(result.length).toEqual(2); expect(result).toEqual( expect.arrayContaining([ - { locationId: undefined, entity: expect.objectContaining(e1) }, - { locationId: undefined, entity: expect.objectContaining(e2) }, + { + locationId: undefined, + entity: expect.objectContaining({ kind: 'k1' }), + }, + { + locationId: undefined, + entity: expect.objectContaining({ kind: 'k2' }), + }, ]), ); }); @@ -318,15 +327,17 @@ describe('Database', () => { it('can get all specific entities for matching filters (naive case)', async () => { const catalog = new Database(database, getVoidLogger()); const entities: Entity[] = [ - { apiVersion: 'a', kind: 'b' }, + { apiVersion: 'a', kind: 'k1', metadata: { name: 'n' } }, { apiVersion: 'a', - kind: 'b', + kind: 'k2', + metadata: { name: 'n' }, spec: { c: 'some' }, }, { apiVersion: 'a', - kind: 'b', + kind: 'k3', + metadata: { name: 'n' }, spec: { c: null }, }, ]; @@ -340,27 +351,32 @@ describe('Database', () => { await expect( catalog.transaction(async tx => catalog.entities(tx, [ - { key: 'kind', values: ['b'] }, + { key: 'kind', values: ['k2'] }, { key: 'spec.c', values: ['some'] }, ]), ), ).resolves.toEqual([ - { locationId: undefined, entity: expect.objectContaining(entities[1]) }, + { + locationId: undefined, + entity: expect.objectContaining({ kind: 'k2' }), + }, ]); }); it('can get all specific entities for matching filters with nulls (both missing and literal null value)', async () => { const catalog = new Database(database, getVoidLogger()); const entities: Entity[] = [ - { apiVersion: 'a', kind: 'b' }, + { apiVersion: 'a', kind: 'k1', metadata: { name: 'n' } }, { apiVersion: 'a', - kind: 'b', + kind: 'k2', + metadata: { name: 'n' }, spec: { c: 'some' }, }, { apiVersion: 'a', - kind: 'b', + kind: 'k3', + metadata: { name: 'n' }, spec: { c: null }, }, ]; @@ -373,7 +389,7 @@ describe('Database', () => { const rows = await catalog.transaction(async tx => catalog.entities(tx, [ - { key: 'kind', values: ['b'] }, + { key: 'apiVersion', values: ['a'] }, { key: 'spec.c', values: [null, 'some'] }, ]), ); @@ -383,15 +399,15 @@ describe('Database', () => { expect.arrayContaining([ { locationId: undefined, - entity: expect.objectContaining(entities[0]), + entity: expect.objectContaining({ kind: 'k1' }), }, { locationId: undefined, - entity: expect.objectContaining(entities[1]), + entity: expect.objectContaining({ kind: 'k2' }), }, { locationId: undefined, - entity: expect.objectContaining(entities[2]), + entity: expect.objectContaining({ kind: 'k3' }), }, ]), ); diff --git a/plugins/catalog-backend/src/database/Database.ts b/plugins/catalog-backend/src/database/Database.ts index b79020eb85..ea823a3da7 100644 --- a/plugins/catalog-backend/src/database/Database.ts +++ b/plugins/catalog-backend/src/database/Database.ts @@ -47,11 +47,7 @@ function getStrippedMetadata(metadata: EntityMeta): EntityMeta { return output; } -function serializeMetadata(metadata: EntityMeta | undefined): string | null { - if (!metadata) { - return null; - } - +function serializeMetadata(metadata: EntityMeta): string { return JSON.stringify(getStrippedMetadata(metadata)); } @@ -68,14 +64,14 @@ function toEntityRow( entity: Entity, ): DbEntitiesRow { return { - id: entity.metadata!.uid!, + id: entity.metadata.uid!, location_id: locationId || null, - etag: entity.metadata!.etag!, - generation: entity.metadata!.generation!, + etag: entity.metadata.etag!, + generation: entity.metadata.generation!, api_version: entity.apiVersion, kind: entity.kind, - name: entity.metadata!.name || null, - namespace: entity.metadata!.namespace || null, + name: entity.metadata.name || null, + namespace: entity.metadata.namespace || null, metadata: serializeMetadata(entity.metadata), spec: serializeSpec(entity.spec), }; @@ -86,17 +82,13 @@ function toEntityResponse(row: DbEntitiesRow): DbEntityResponse { apiVersion: row.api_version, kind: row.kind, metadata: { + ...(JSON.parse(row.metadata) as Entity['metadata']), uid: row.id, etag: row.etag, generation: Number(row.generation), // cast because of sqlite }, }; - if (row.metadata) { - const metadata = JSON.parse(row.metadata) as Entity['metadata']; - entity.metadata = { ...entity.metadata, ...metadata }; - } - if (row.spec) { const spec = JSON.parse(row.spec); entity.spec = spec; @@ -177,11 +169,11 @@ export class Database { tx: Knex.Transaction, request: DbEntityRequest, ): Promise { - if (request.entity.metadata?.uid !== undefined) { + if (request.entity.metadata.uid !== undefined) { throw new InputError('May not specify uid for new entities'); - } else if (request.entity.metadata?.etag !== undefined) { + } else if (request.entity.metadata.etag !== undefined) { throw new InputError('May not specify etag for new entities'); - } else if (request.entity.metadata?.generation !== undefined) { + } else if (request.entity.metadata.generation !== undefined) { throw new InputError('May not specify generation for new entities'); } @@ -294,9 +286,9 @@ export class Database { if (oldRow.metadata) { const oldMetadata = JSON.parse(oldRow.metadata) as EntityMeta; if (oldMetadata.annotations) { - newEntity.metadata!.annotations = { + newEntity.metadata.annotations = { ...oldMetadata.annotations, - ...newEntity.metadata!.annotations, + ...newEntity.metadata.annotations, }; } } diff --git a/plugins/catalog-backend/src/database/DatabaseManager.test.ts b/plugins/catalog-backend/src/database/DatabaseManager.test.ts index 5f85b5ce15..8a5201785c 100644 --- a/plugins/catalog-backend/src/database/DatabaseManager.test.ts +++ b/plugins/catalog-backend/src/database/DatabaseManager.test.ts @@ -36,14 +36,14 @@ describe('DatabaseManager', () => { readLocation: jest.fn(), }; const policy: EntityPolicy = { - apply: jest.fn(), + enforce: jest.fn(), }; await expect( DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()), ).resolves.toBeUndefined(); expect(reader.readLocation).not.toHaveBeenCalled(); - expect(policy.apply).not.toHaveBeenCalled(); + expect(policy.enforce).not.toHaveBeenCalled(); }); it('can update a single location', async () => { @@ -78,7 +78,7 @@ describe('DatabaseManager', () => { ), }; const policy: EntityPolicy = { - apply: jest.fn(() => Promise.resolve(desc)), + enforce: jest.fn(() => Promise.resolve(desc)), }; await expect( @@ -126,7 +126,7 @@ describe('DatabaseManager', () => { ), }; const policy: EntityPolicy = { - apply: jest.fn(() => Promise.resolve(desc)), + enforce: jest.fn(() => Promise.resolve(desc)), }; await expect( @@ -178,7 +178,9 @@ describe('DatabaseManager', () => { ), }; const policy: EntityPolicy = { - apply: jest.fn(() => Promise.reject(new Error('parser error message'))), + enforce: jest.fn(() => + Promise.reject(new Error('parser error message')), + ), }; await expect( @@ -225,7 +227,9 @@ describe('DatabaseManager', () => { ), }; const policy: EntityPolicy = { - apply: jest.fn(() => Promise.reject(new Error('parser error message'))), + enforce: jest.fn(() => + Promise.reject(new Error('parser error message')), + ), }; await expect( diff --git a/plugins/catalog-backend/src/database/DatabaseManager.ts b/plugins/catalog-backend/src/database/DatabaseManager.ts index dff589c525..05b6eaef8d 100644 --- a/plugins/catalog-backend/src/database/DatabaseManager.ts +++ b/plugins/catalog-backend/src/database/DatabaseManager.ts @@ -19,9 +19,9 @@ import Knex from 'knex'; import lodash from 'lodash'; import path from 'path'; import { Logger } from 'winston'; +import { IngestionModel } from '../ingestion/types'; import { Database } from './Database'; import { DatabaseLocationUpdateLogStatus, DbEntityRequest } from './types'; -import { IngestionModel } from '../ingestion/types'; export class DatabaseManager { public static async createDatabase( @@ -86,7 +86,7 @@ export class DatabaseManager { } try { - const entity = await entityPolicy.apply(readerItem.data); + const entity = await entityPolicy.enforce(readerItem.data); await DatabaseManager.refreshSingleEntity( database, location.id, @@ -96,14 +96,14 @@ export class DatabaseManager { await DatabaseManager.logUpdateSuccess( database, location.id, - entity.metadata!.name, + entity.metadata.name, ); } catch (error) { await DatabaseManager.logUpdateFailure( database, location.id, error, - readerItem.data.metadata?.name, + readerItem.data.metadata.name, ); } } diff --git a/plugins/catalog-backend/src/database/migrations/20200511113813_init.ts b/plugins/catalog-backend/src/database/migrations/20200511113813_init.ts index f029871a70..2ff06c4046 100644 --- a/plugins/catalog-backend/src/database/migrations/20200511113813_init.ts +++ b/plugins/catalog-backend/src/database/migrations/20200511113813_init.ts @@ -76,7 +76,7 @@ export async function up(knex: Knex): Promise { .comment('The metadata.namespace field of the entity'); table .string('metadata') - .nullable() + .notNullable() .comment('The entire metadata JSON blob of the entity'); table .string('spec') diff --git a/plugins/catalog-backend/src/database/search.test.ts b/plugins/catalog-backend/src/database/search.test.ts index 7ad06aee14..8f011fb250 100644 --- a/plugins/catalog-backend/src/database/search.test.ts +++ b/plugins/catalog-backend/src/database/search.test.ts @@ -102,14 +102,15 @@ describe('search', () => { const input: Entity = { apiVersion: 'a', kind: 'b', + metadata: { name: 'n' }, }; expect(buildEntitySearch('eid', input)).toEqual([ - { entity_id: 'eid', key: 'metadata.name', value: null }, + { entity_id: 'eid', key: 'metadata.name', value: 'n' }, { entity_id: 'eid', key: 'metadata.namespace', value: null }, { entity_id: 'eid', key: 'metadata.uid', value: null }, { entity_id: 'eid', key: 'apiVersion', value: 'a' }, { entity_id: 'eid', key: 'kind', value: 'b' }, - { entity_id: 'eid', key: 'name', value: null }, + { entity_id: 'eid', key: 'name', value: 'n' }, { entity_id: 'eid', key: 'namespace', value: null }, { entity_id: 'eid', key: 'uid', value: null }, ]); diff --git a/plugins/catalog-backend/src/database/search.ts b/plugins/catalog-backend/src/database/search.ts index fcacf1a9f2..87fc59185d 100644 --- a/plugins/catalog-backend/src/database/search.ts +++ b/plugins/catalog-backend/src/database/search.ts @@ -127,17 +127,17 @@ export function buildEntitySearch( { entity_id: entityId, key: 'metadata.name', - value: toValue(entity.metadata?.name), + value: toValue(entity.metadata.name), }, { entity_id: entityId, key: 'metadata.namespace', - value: toValue(entity.metadata?.namespace), + value: toValue(entity.metadata.namespace), }, { entity_id: entityId, key: 'metadata.uid', - value: toValue(entity.metadata?.uid), + value: toValue(entity.metadata.uid), }, ]; diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts index 19adffbb9f..0b9485242f 100644 --- a/plugins/catalog-backend/src/database/types.ts +++ b/plugins/catalog-backend/src/database/types.ts @@ -26,7 +26,7 @@ export type DbEntitiesRow = { namespace: string | null; etag: string; generation: number; - metadata: string | null; + metadata: string; spec: string | null; }; diff --git a/plugins/catalog-backend/src/ingestion/IngestionModels.ts b/plugins/catalog-backend/src/ingestion/IngestionModels.ts index 616f1b49ae..def6f1fa5c 100644 --- a/plugins/catalog-backend/src/ingestion/IngestionModels.ts +++ b/plugins/catalog-backend/src/ingestion/IngestionModels.ts @@ -60,7 +60,7 @@ export class IngestionModels implements IngestionModel { result.push(item); } else { try { - const output = await this.entityPolicy.apply(item.data); + const output = await this.entityPolicy.enforce(item.data); result.push({ type: 'data', data: output }); } catch (e) { result.push({ type: 'error', error: e }); diff --git a/plugins/catalog-backend/src/ingestion/source/index.ts b/plugins/catalog-backend/src/ingestion/source/index.ts index 3ed1063878..db7aa2f0bd 100644 --- a/plugins/catalog-backend/src/ingestion/source/index.ts +++ b/plugins/catalog-backend/src/ingestion/source/index.ts @@ -17,4 +17,4 @@ export { LocationReaders } from './LocationReaders'; export { FileLocationReader } from './readers/FileLocationReader'; export { GitHubLocationReader } from './readers/GitHubLocationReader'; -export { LocationReader } from './readers/types'; +export type { LocationReader } from './readers/types'; diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts index ed9024bb87..8c7946816c 100644 --- a/plugins/catalog-backend/src/service/router.test.ts +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -38,7 +38,9 @@ class MockLocationsCatalog implements LocationsCatalog { describe('createRouter', () => { describe('entities', () => { it('happy path: lists entities', async () => { - const entities: Entity[] = [{ apiVersion: 'a', kind: 'b' }]; + const entities: Entity[] = [ + { apiVersion: 'a', kind: 'b', metadata: { name: 'n' } }, + ]; const catalog = new MockEntitiesCatalog(); catalog.entities.mockResolvedValueOnce(entities); diff --git a/plugins/catalog-backend/src/setupTests.ts b/plugins/catalog-backend/src/setupTests.ts index 3fa7cb04b4..a3b2b7e123 100644 --- a/plugins/catalog-backend/src/setupTests.ts +++ b/plugins/catalog-backend/src/setupTests.ts @@ -15,3 +15,4 @@ */ require('jest-fetch-mock').enableMocks(); +export {}; diff --git a/plugins/circleci/src/api/index.ts b/plugins/circleci/src/api/index.ts index 26b0213e3a..64a33e4c13 100644 --- a/plugins/circleci/src/api/index.ts +++ b/plugins/circleci/src/api/index.ts @@ -28,7 +28,8 @@ import { } from 'circleci-api'; import { createApiRef } from '@backstage/core'; -export { BuildWithSteps, BuildStepAction, BuildSummary, GitType }; +export { GitType }; +export type { BuildWithSteps, BuildStepAction, BuildSummary }; export const circleCIApiRef = createApiRef({ id: 'plugin.circleci.service', diff --git a/plugins/identity-backend/src/index.test.ts b/plugins/identity-backend/src/index.test.ts index b3e2f19771..4fca4ca746 100644 --- a/plugins/identity-backend/src/index.test.ts +++ b/plugins/identity-backend/src/index.test.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import '@backstage/backend-common'; + describe('test', () => { it('unbreaks the test runner', () => { expect(true).toBeTruthy(); diff --git a/plugins/identity-backend/src/setupTests.ts b/plugins/identity-backend/src/setupTests.ts index 3fa7cb04b4..a3b2b7e123 100644 --- a/plugins/identity-backend/src/setupTests.ts +++ b/plugins/identity-backend/src/setupTests.ts @@ -15,3 +15,4 @@ */ require('jest-fetch-mock').enableMocks(); +export {}; diff --git a/yarn.lock b/yarn.lock index 74b9e451b0..8439f5b0b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3071,7 +3071,7 @@ regenerator-runtime "^0.13.3" util-deprecate "^1.0.2" -"@storybook/addons@5.3.18", "@storybook/addons@^5.3.17": +"@storybook/addons@5.3.18": version "5.3.18" resolved "https://registry.npmjs.org/@storybook/addons/-/addons-5.3.18.tgz#5cbba6407ef7a802041c5ee831473bc3bed61f64" integrity sha512-ZQjDgTUDFRLvAiBg2d8FgPgghfQ+9uFyXQbtiGlTBLinrPCeQd7J86qiUES0fcGoohCCw0wWKtvB0WF2z1XNDg== @@ -3084,6 +3084,19 @@ global "^4.3.2" util-deprecate "^1.0.2" +"@storybook/addons@^5.3.17": + version "5.3.19" + resolved "https://registry.npmjs.org/@storybook/addons/-/addons-5.3.19.tgz#3a7010697afd6df9a41b8c8a7351d9a06ff490a4" + integrity sha512-Ky/k22p6i6FVNvs1VhuFyGvYJdcp+FgXqFgnPyY/OXJW/vPDapdElpTpHJZLFI9I2FQBDcygBPU5RXkumQ+KUQ== + dependencies: + "@storybook/api" "5.3.19" + "@storybook/channels" "5.3.19" + "@storybook/client-logger" "5.3.19" + "@storybook/core-events" "5.3.19" + core-js "^3.0.1" + global "^4.3.2" + util-deprecate "^1.0.2" + "@storybook/api@5.3.18": version "5.3.18" resolved "https://registry.npmjs.org/@storybook/api/-/api-5.3.18.tgz#95582ab90d947065e0e34ed603650a3630dcbd16" @@ -3110,6 +3123,32 @@ telejson "^3.2.0" util-deprecate "^1.0.2" +"@storybook/api@5.3.19": + version "5.3.19" + resolved "https://registry.npmjs.org/@storybook/api/-/api-5.3.19.tgz#77f15e9e2eee59fe1ddeaba1ef39bc34713a6297" + integrity sha512-U/VzDvhNCPmw2igvJYNNM+uwJCL+3teiL6JmuoL4/cmcqhI6IqqG9dZmMP1egoCd19wXEP7rnAfB/VcYVg41dQ== + dependencies: + "@reach/router" "^1.2.1" + "@storybook/channels" "5.3.19" + "@storybook/client-logger" "5.3.19" + "@storybook/core-events" "5.3.19" + "@storybook/csf" "0.0.1" + "@storybook/router" "5.3.19" + "@storybook/theming" "5.3.19" + "@types/reach__router" "^1.2.3" + core-js "^3.0.1" + fast-deep-equal "^2.0.1" + global "^4.3.2" + lodash "^4.17.15" + memoizerific "^1.11.3" + prop-types "^15.6.2" + react "^16.8.3" + semver "^6.0.0" + shallow-equal "^1.1.0" + store2 "^2.7.1" + telejson "^3.2.0" + util-deprecate "^1.0.2" + "@storybook/channel-postmessage@5.3.18": version "5.3.18" resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-5.3.18.tgz#93d46740b5cc9b36ddd073f0715b54c4959953bf" @@ -3128,6 +3167,13 @@ dependencies: core-js "^3.0.1" +"@storybook/channels@5.3.19": + version "5.3.19" + resolved "https://registry.npmjs.org/@storybook/channels/-/channels-5.3.19.tgz#65ad7cd19d70aa5eabbb2e5e39ceef5e510bcb7f" + integrity sha512-38seaeyshRGotTEZJppyYMg/Vx2zRKgFv1L6uGqkJT0LYoNSYtJhsiNFCJ2/KUJu2chAJ/j8h80bpVBVLQ/+WA== + dependencies: + core-js "^3.0.1" + "@storybook/client-api@5.3.18": version "5.3.18" resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-5.3.18.tgz#e71041796f95888de0e4524734418e6b120b060a" @@ -3158,6 +3204,13 @@ dependencies: core-js "^3.0.1" +"@storybook/client-logger@5.3.19": + version "5.3.19" + resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-5.3.19.tgz#fbbd186e82102eaca1d6a5cca640271cae862921" + integrity sha512-nHftT9Ow71YgAd2/tsu79kwKk30mPuE0sGRRUHZVyCRciGFQweKNOS/6xi2Aq+WwBNNjPKNlbgxwRt1yKe1Vkg== + dependencies: + core-js "^3.0.1" + "@storybook/components@5.3.18": version "5.3.18" resolved "https://registry.npmjs.org/@storybook/components/-/components-5.3.18.tgz#528f6ab1660981e948993a04b407a6fad7751589" @@ -3192,6 +3245,13 @@ dependencies: core-js "^3.0.1" +"@storybook/core-events@5.3.19": + version "5.3.19" + resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-5.3.19.tgz#18020cd52e0d8ef0973a8e9622a10d5f99796f79" + integrity sha512-lh78ySqMS7pDdMJAQAe35d1I/I4yPTqp09Cq0YIYOxx9BQZhah4DZTV1QIZt22H5p2lPb5MWLkWSxBaexZnz8A== + dependencies: + core-js "^3.0.1" + "@storybook/core@5.3.18": version "5.3.18" resolved "https://registry.npmjs.org/@storybook/core/-/core-5.3.18.tgz#3f3c0498275826c1cc4368aba203ac17a6ae5c9c" @@ -3332,6 +3392,21 @@ qs "^6.6.0" util-deprecate "^1.0.2" +"@storybook/router@5.3.19": + version "5.3.19" + resolved "https://registry.npmjs.org/@storybook/router/-/router-5.3.19.tgz#0f783b85658f99e4007f74347ad7ef17dbf7fc3a" + integrity sha512-yNClpuP7BXQlBTRf6Ggle3/R349/k6kvI5Aim4jf6X/2cFVg2pzBXDAF41imNm9PcvdxwabQLm6I48p7OvKr/w== + dependencies: + "@reach/router" "^1.2.1" + "@storybook/csf" "0.0.1" + "@types/reach__router" "^1.2.3" + core-js "^3.0.1" + global "^4.3.2" + lodash "^4.17.15" + memoizerific "^1.11.3" + qs "^6.6.0" + util-deprecate "^1.0.2" + "@storybook/source-loader@5.3.18": version "5.3.18" resolved "https://registry.npmjs.org/@storybook/source-loader/-/source-loader-5.3.18.tgz#39ba28d9664ab8204d6b04ee757772369931e7e5" @@ -3366,6 +3441,24 @@ resolve-from "^5.0.0" ts-dedent "^1.1.0" +"@storybook/theming@5.3.19": + version "5.3.19" + resolved "https://registry.npmjs.org/@storybook/theming/-/theming-5.3.19.tgz#177d9819bd64f7a1a6ea2f1920ffa5baf9a5f467" + integrity sha512-ecG+Rq3hc1GOzKHamYnD4wZ0PEP9nNg0mXbC3RhbxfHj+pMMCWWmx9B2Uu75SL1PTT8WcfkFO0hU/0IO84Pzlg== + dependencies: + "@emotion/core" "^10.0.20" + "@emotion/styled" "^10.0.17" + "@storybook/client-logger" "5.3.19" + core-js "^3.0.1" + deep-object-diff "^1.1.0" + emotion-theming "^10.0.19" + global "^4.3.2" + memoizerific "^1.11.3" + polished "^3.3.1" + prop-types "^15.7.2" + resolve-from "^5.0.0" + ts-dedent "^1.1.0" + "@storybook/ui@5.3.18": version "5.3.18" resolved "https://registry.npmjs.org/@storybook/ui/-/ui-5.3.18.tgz#c66f6d94a3c50bb706f4d5b1d5592439110f16f0" @@ -14189,9 +14282,9 @@ marked@^0.8.0: integrity sha512-tZfJS8uE0zpo7xpTffwFwYRfW9AzNcdo04Qcjs+C9+oCy8MSRD2reD5iDVtYx8mtLaqsGughw/YLlcwNxAHA1g== material-table@^1.58.0: - version "1.58.0" - resolved "https://registry.npmjs.org/material-table/-/material-table-1.58.0.tgz#1902f88b74436ce880b234c7c713f0e34a7e2f9d" - integrity sha512-5xiiKERNZv9Zai2TMT5d9LRLrKlofAFe2YZZzxP7cJ9DIZC8HcaflunyIVHYTzK1yFS51TqjDvHIqxYLIqdahQ== + version "1.58.2" + resolved "https://registry.npmjs.org/material-table/-/material-table-1.58.2.tgz#dc0d19652848e6bb92f747d122bd7d4681cca6dc" + integrity sha512-s/m6ebyXFXmg07zxv1Fl6qPySKaiQhASXaOB3ubRKUFA1DkryUy3PGSEVWTjUYnRyi63kGq+N6b5nsokLR6m5A== dependencies: "@date-io/date-fns" "^1.1.0" "@material-ui/pickers" "^3.2.2" @@ -18995,10 +19088,10 @@ source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: source-map-url "^0.4.0" urix "^0.1.0" -source-map-support@^0.5.6, source-map-support@~0.5.12: - version "0.5.16" - resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" - integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== +source-map-support@^0.5.17, source-map-support@^0.5.6, source-map-support@~0.5.12: + version "0.5.19" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -20323,14 +20416,14 @@ ts-loader@^7.0.4: semver "^6.0.0" ts-node@^8.6.2: - version "8.8.1" - resolved "https://registry.npmjs.org/ts-node/-/ts-node-8.8.1.tgz#7c4d3e9ed33aa703b64b28d7f9d194768be5064d" - integrity sha512-10DE9ONho06QORKAaCBpPiFCdW+tZJuY/84tyypGtl6r+/C7Asq0dhqbRZURuUlLQtZxxDvT8eoj8cGW0ha6Bg== + version "8.10.2" + resolved "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d" + integrity sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA== dependencies: arg "^4.1.0" diff "^4.0.1" make-error "^1.1.1" - source-map-support "^0.5.6" + source-map-support "^0.5.17" yn "3.1.1" ts-pnp@^1.1.2: