Merge branch 'master' into feat/allo-to-provide-custom-techdocs-generator
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend-module-guest-provider': minor
|
||||
---
|
||||
|
||||
Adds a new guest provider that maps guest users to actual tokens. This also shifts the default guest login to `user:development/guest` to reduce overlap with your production/real data. To change that (or set it back to the old default, use the new `auth.providers.guest.userEntityRef` config key) like so,
|
||||
|
||||
```yaml title=app-config.yaml
|
||||
auth:
|
||||
providers:
|
||||
guest:
|
||||
userEntityRef: user:default/guest
|
||||
```
|
||||
|
||||
This also adds a new property to control the ownership entity refs,
|
||||
|
||||
```yaml title=app-config.yaml
|
||||
auth:
|
||||
providers:
|
||||
guest:
|
||||
ownershipEntityRefs:
|
||||
- guests
|
||||
- development/custom
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
Bumped create-app version.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend-module-elasticsearch': patch
|
||||
'@backstage/plugin-search-backend-module-pg': patch
|
||||
---
|
||||
|
||||
Start importing `QueryTranslator`, `QueryRequestOptions` and `SearchEngine` from the `@backstage/plugin-search-backend-node`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-node': patch
|
||||
---
|
||||
|
||||
Fixed file corruption for non UTF-8 data in fetch contents
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-test-utils': patch
|
||||
---
|
||||
|
||||
Added `mockServices.userInfo`, which now also automatically is made available in test backends.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/theme': patch
|
||||
---
|
||||
|
||||
Exported `defaultTypography` to make adjusting these values in a custom theme easier
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-notifications-backend': patch
|
||||
'@backstage/plugin-notifications': patch
|
||||
---
|
||||
|
||||
The Notifications can be newly filtered based on the Created Date.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
Support i18n for core components
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend-module-confluence-to-markdown': patch
|
||||
'@backstage/plugin-scaffolder-backend-module-bitbucket-server': patch
|
||||
'@backstage/plugin-scaffolder-backend-module-bitbucket-cloud': patch
|
||||
'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch
|
||||
'@backstage/plugin-scaffolder-backend-module-bitbucket': patch
|
||||
'@backstage/plugin-scaffolder-backend-module-gerrit': patch
|
||||
'@backstage/plugin-scaffolder-backend-module-github': patch
|
||||
'@backstage/plugin-scaffolder-backend-module-gitlab': patch
|
||||
'@backstage/plugin-scaffolder-backend-module-sentry': patch
|
||||
'@backstage/plugin-scaffolder-backend-module-yeoman': patch
|
||||
'@backstage/plugin-scaffolder-backend-module-azure': patch
|
||||
'@backstage/plugin-scaffolder-backend-module-gitea': patch
|
||||
'@backstage/plugin-scaffolder-backend-module-rails': patch
|
||||
'@backstage/plugin-scaffolder-node-test-utils': minor
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Introduced `createMockActionContext` to unify the way of creating scaffolder mock context.
|
||||
|
||||
It will help to maintain tests in a long run during structural changes of action context.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
Added the `UserInfoApi` as both an optional input and as an output for `createLegacyAuthAdapters`
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend-module-techdocs': patch
|
||||
'@backstage/plugin-search-backend-module-catalog': patch
|
||||
'@backstage/plugin-search-backend-module-explore': patch
|
||||
---
|
||||
|
||||
Migrated to support new auth services.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search-common': patch
|
||||
---
|
||||
|
||||
Deprecate `QueryTranslator`, `QueryRequestOptions` and `SearchEngine` in favor of the types exported from `@backstage/plugin-search-backend-node`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search': patch
|
||||
---
|
||||
|
||||
Removes ADR from the default set of search filters
|
||||
+109
-2
@@ -274,7 +274,114 @@
|
||||
"@backstage/plugin-vault": "0.1.25",
|
||||
"@backstage/plugin-vault-backend": "0.4.3",
|
||||
"@backstage/plugin-vault-node": "0.1.3",
|
||||
"@backstage/plugin-xcmetrics": "0.2.48"
|
||||
"@backstage/plugin-xcmetrics": "0.2.48",
|
||||
"@backstage/plugin-auth-backend-module-guest-provider": "0.0.0",
|
||||
"@backstage/plugin-scaffolder-node-test-utils": "0.0.1"
|
||||
},
|
||||
"changesets": []
|
||||
"changesets": [
|
||||
"big-yaks-film",
|
||||
"breezy-cycles-count",
|
||||
"bright-bulldogs-whisper",
|
||||
"calm-pans-work",
|
||||
"chilled-dolls-accept",
|
||||
"chilled-dolphins-tap",
|
||||
"chilled-goats-matter",
|
||||
"clever-eagles-boil",
|
||||
"cold-boats-sell",
|
||||
"cold-dolphins-raise",
|
||||
"create-app-1709052411",
|
||||
"cyan-dryers-share",
|
||||
"dirty-apes-divide",
|
||||
"dry-impalas-serve",
|
||||
"eight-fireants-crash",
|
||||
"eighty-suits-admire",
|
||||
"eleven-cows-learn",
|
||||
"empty-wolves-rule",
|
||||
"fast-buses-exercise",
|
||||
"fifty-insects-yell",
|
||||
"fifty-moons-study",
|
||||
"five-beers-accept",
|
||||
"five-hats-accept",
|
||||
"five-mayflies-juggle",
|
||||
"flat-badgers-attack",
|
||||
"forty-oranges-joke",
|
||||
"fresh-rings-tell",
|
||||
"friendly-coats-travel",
|
||||
"friendly-news-sin",
|
||||
"funny-flies-collect",
|
||||
"healthy-experts-rhyme",
|
||||
"heavy-coats-sniff",
|
||||
"hungry-points-burn",
|
||||
"itchy-news-drive",
|
||||
"kind-pants-speak",
|
||||
"kind-students-cross",
|
||||
"late-turkeys-remember",
|
||||
"lazy-needles-lick",
|
||||
"lazy-terms-shake",
|
||||
"lemon-lemons-sparkle",
|
||||
"long-emus-talk",
|
||||
"loud-dolls-exist",
|
||||
"lovely-donkeys-kneel",
|
||||
"modern-impalas-add",
|
||||
"neat-owls-pump",
|
||||
"nervous-lions-suffer",
|
||||
"nice-beans-wait",
|
||||
"odd-toys-wonder",
|
||||
"old-ducks-fetch",
|
||||
"olive-mails-tell",
|
||||
"perfect-taxis-give",
|
||||
"polite-tips-begin",
|
||||
"polite-zoos-pay",
|
||||
"poor-beans-cross",
|
||||
"poor-ladybugs-smell",
|
||||
"pretty-boats-promise",
|
||||
"purple-kiwis-complain",
|
||||
"rare-dryers-check",
|
||||
"red-taxis-swim",
|
||||
"renovate-0300bde",
|
||||
"renovate-08c5b50",
|
||||
"renovate-1c2c49d",
|
||||
"renovate-58582bb",
|
||||
"renovate-5d40e90",
|
||||
"renovate-6a81dd3",
|
||||
"renovate-755938a",
|
||||
"renovate-7aa519f",
|
||||
"renovate-8f23b96",
|
||||
"renovate-914f0df",
|
||||
"renovate-9850908",
|
||||
"renovate-ea48bac",
|
||||
"rude-masks-tan",
|
||||
"rude-sheep-jam",
|
||||
"selfish-glasses-cheer",
|
||||
"selfish-walls-perform",
|
||||
"silver-flowers-trade",
|
||||
"silver-impalas-run",
|
||||
"six-grapes-sniff",
|
||||
"six-nails-hammer",
|
||||
"six-sloths-listen",
|
||||
"sixty-queens-mix",
|
||||
"slimy-trainers-attend",
|
||||
"slow-readers-clap",
|
||||
"smart-owls-tease",
|
||||
"soft-grapes-cough",
|
||||
"soft-otters-report",
|
||||
"sour-olives-carry",
|
||||
"spicy-dragons-sin",
|
||||
"tasty-beans-confess",
|
||||
"ten-spoons-help",
|
||||
"tender-carrots-care",
|
||||
"thick-pillows-develop",
|
||||
"thin-spiders-do",
|
||||
"thirty-shirts-allow",
|
||||
"tiny-books-destroy",
|
||||
"tiny-bugs-enjoy",
|
||||
"tricky-months-hug",
|
||||
"two-planets-beam",
|
||||
"two-snails-fry",
|
||||
"unlucky-jobs-report",
|
||||
"unlucky-lizards-suffer",
|
||||
"violet-rocks-rescue",
|
||||
"wet-sheep-reply",
|
||||
"young-flies-wash"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': minor
|
||||
---
|
||||
|
||||
Migrated to support new auth services. The `CatalogBuilder.create` method now accepts a `discovery` option, which is recommended to forward from the plugin environment, as it will otherwise fall back to use the `HostDiscovery` implementation.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes-backend': minor
|
||||
---
|
||||
|
||||
**BREAKING**: The `KubernetesBuilder.createBuilder` method now requires the `discovery` service to be forwarded from the plugin environment. This is part of the migration to support new auth services.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
`SignInPage`'s `'guest'` provider now supports the `@backstage/plugin-auth-backend-module-guest-provider` package to generate tokens. It will continue to use the old frontend-only auth as a fallback.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-adr': patch
|
||||
---
|
||||
|
||||
Remove unused package dependencies
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-import': patch
|
||||
---
|
||||
|
||||
Fixed an issue generating a wrong entity link at the end of the import process
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend': patch
|
||||
---
|
||||
|
||||
Update the router to use the new `auth` services, it now accepts an optional discovery service option to get credentials for the permission service.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': minor
|
||||
'@backstage/plugin-scaffolder-node': patch
|
||||
---
|
||||
|
||||
Introducing checkpoints for scaffolder task action idempotency
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-devtools-backend': minor
|
||||
---
|
||||
|
||||
**BREAKING**: The `createRouter` method now requires the `discovery` service to be forwarded from the plugin environment. This is part of the migration to support new auth services.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-app-api': patch
|
||||
---
|
||||
|
||||
Made the `DefaultUserInfoService` claims check stricter
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-tech-insights-node': minor
|
||||
---
|
||||
|
||||
**BREAKING**: The `FactRetrieverContext` type now contains an additional `auth` field.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-notifications-backend': minor
|
||||
'@backstage/plugin-notifications': minor
|
||||
'@backstage/plugin-notifications-common': patch
|
||||
---
|
||||
|
||||
The Notifications frontend has been redesigned towards list view with condensed row details. The 'done' attribute has been removed to keep the Notifications aligned with the idea of a messaging system instead of a task manager.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-tech-insights-backend': patch
|
||||
---
|
||||
|
||||
Added support for the new `AuthService`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-permission-backend': patch
|
||||
---
|
||||
|
||||
Migrated to use the new auth services introduced in [BEP-0003](https://github.com/backstage/backstage/blob/master/beps/0003-auth-architecture-evolution/README.md).
|
||||
|
||||
The `createRouter` function now accepts `auth`, `httpAuth` and `userInfo` options. Theses are used internally to support the new backend system, and can be ignored.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend-node': patch
|
||||
---
|
||||
|
||||
Exports `QueryTranslator`, `QueryRequestOptions` and `SearchEngine` types. These new types were extracted from the `@backstage/plugin-search-common` package and the `token` property was deprecated in favor of the a new credentials one.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Allow the `spec.target` field to be searchable in the catalog table for locations. Previously, only the `spec.targets` field was be searchable. This makes locations generated by providers such as the `GithubEntityProvider` searchable in the catalog table. [#23098](https://github.com/backstage/backstage/issues/23098)
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
with:
|
||||
context: './example-app'
|
||||
file: ./example-app/packages/backend/Dockerfile
|
||||
push: ${{ (github.event_name == "repository_dispatch") && (github.event.action == "release-published") }}
|
||||
push: ${{ (github.event_name == 'repository_dispatch') && (github.event.action == 'release-published') }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository_owner }}/backstage:latest
|
||||
|
||||
+4
-1
@@ -242,7 +242,7 @@ catalog:
|
||||
- Domain
|
||||
- Location
|
||||
providers:
|
||||
openapi:
|
||||
backstageOpenapi:
|
||||
plugins:
|
||||
- catalog
|
||||
- search
|
||||
@@ -399,6 +399,9 @@ auth:
|
||||
scopes: ${AUTH_ATLASSIAN_SCOPES}
|
||||
myproxy:
|
||||
development: {}
|
||||
guest:
|
||||
development: {}
|
||||
|
||||
costInsights:
|
||||
engineerCost: 200000
|
||||
engineerThreshold: 0.5
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
id: provider
|
||||
title: Guest Authentication Provider
|
||||
sidebar_label: Guest
|
||||
description: Adding a guest authentication provider in Backstage
|
||||
---
|
||||
|
||||
Audience: Admins or developers
|
||||
|
||||
## Summary
|
||||
|
||||
The goal of this guide is to get you set up with a guest authentication provider that emits tokens. This is different than the old guest authentication that is purely stored on the frontend and does not have tokens. The main reason you'd want to use this provider is to use permissioned plugins.
|
||||
|
||||
:::caution
|
||||
This provider should only ever be enabled for `development`. To prevent unauthorized access to your data, this package is _explicitly_ disabled for non-development environments.
|
||||
:::
|
||||
|
||||
## Installation
|
||||
|
||||
### Backend
|
||||
|
||||
:::note
|
||||
This will only work with the new backend system. There is no support for this in the old backend.
|
||||
:::
|
||||
|
||||
Add the `@backstage/plugin-auth-backend-module-guest-provider` to your backend installation.
|
||||
|
||||
```
|
||||
yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-guest-provider
|
||||
```
|
||||
|
||||
Then, add it to your backend's `index.ts` file,
|
||||
|
||||
```diff
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add('@backstage/plugin-auth-backend');
|
||||
+backend.add('@backstage/plugin-auth-backend-module-guest-provider');
|
||||
|
||||
await backend.start();
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
Add the following to your `SignInPage` providers,
|
||||
|
||||
```diff
|
||||
const providers = [
|
||||
+ 'guest',
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
### Config
|
||||
|
||||
Similar to the other authentication providers, you have to enable the provider in config. Add the following to your `app-config.local.yaml`,
|
||||
|
||||
```diff
|
||||
auth:
|
||||
providers:
|
||||
+ guest:
|
||||
+ userEntityRef: user:default/guest
|
||||
+ development: {}
|
||||
```
|
||||
|
||||
We need to specify that the provider is enabled for the given environment, and as there are no config values for this provider yet, you can just specify an empty object.
|
||||
@@ -56,8 +56,7 @@ Just like plugins, modules also have access to services and can depend on their
|
||||
## Package structure
|
||||
|
||||
A detailed explanation of the package architecture can be found in the
|
||||
[Backstage Architecture
|
||||
Overview](../../overview/architecture-overview.md#package-architecture). The
|
||||
[Backstage Architecture Overview](../../overview/architecture-overview.md#package-architecture). The
|
||||
most important packages to consider for this system are the following:
|
||||
|
||||
- `plugin-<pluginId>-backend` houses the implementation of the backend plugins
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
id: writing-tests-for-actions
|
||||
title: Writing Tests For Actions
|
||||
description: How to write tests for actions
|
||||
---
|
||||
|
||||
Once you created a new action, your own custom one, or you would like to contribute new actions, you have to cover it with
|
||||
Unit tests to be sure that your actions do what they suppose to do.
|
||||
|
||||
Make sure that you cover the most of scenario's, which could happen with the action.
|
||||
One of indispensable part of the test is to supply the context to a handler of action for the execution.
|
||||
We encourage you to use a utility method for that, so your tests are immune to structural changes of context.
|
||||
What is inevitably going to happen during the time.
|
||||
|
||||
Example how to use it:
|
||||
|
||||
```typescript
|
||||
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
|
||||
|
||||
const mockContext = createMockActionContext({
|
||||
input: { repoUrl: 'dev.azure.com?repo=repo&owner=owner&organization=org' },
|
||||
});
|
||||
|
||||
await action.handler(mockContext);
|
||||
|
||||
expect(mockContext.output).toHaveBeenCalledWith(
|
||||
'remoteUrl',
|
||||
'https://dev.azure.com/organization/project/_git/repo',
|
||||
);
|
||||
```
|
||||
|
||||
One thing to be aware about: if you would like to call `createMockActionContext` inside `it`,
|
||||
you have to provide a `workspacePath`. By default, `createMockActionContext` uses
|
||||
`import { createMockDirectory } from '@backstage/backend-test-utils';` to create it for you.
|
||||
This implementation contains a hook inside which creates this limitation. So in this case you can do then:
|
||||
|
||||
```typescript
|
||||
describe('github:autolinks:create', async () => {
|
||||
const workspacePath = createMockDirectory().resolve('workspace');
|
||||
// ...
|
||||
|
||||
it('should call the githubApis for creating alphanumeric autolink reference', async () => {
|
||||
// ...
|
||||
await action.handler(
|
||||
createMockActionContext({
|
||||
input: {
|
||||
repoUrl: 'github.com?repo=repo&owner=owner',
|
||||
keyPrefix: 'TICKET-',
|
||||
urlTemplate: 'https://example.com/TICKET?query=<num>',
|
||||
},
|
||||
workspacePath,
|
||||
}),
|
||||
);
|
||||
//...
|
||||
});
|
||||
});
|
||||
```
|
||||
@@ -4,56 +4,42 @@ title: Customize the look-and-feel of your App
|
||||
description: Documentation on Customizing look and feel of the App
|
||||
---
|
||||
|
||||
Backstage ships with a default theme with a light and dark mode variant. The
|
||||
themes are provided as a part of the
|
||||
[`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme) package,
|
||||
which also includes utilities for customizing the default theme, or creating
|
||||
completely new themes.
|
||||
Backstage ships with a default theme with a light and dark mode variant. The themes are provided as a part of the [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme) package, which also includes utilities for customizing the default theme, or creating completely new themes.
|
||||
|
||||
## Creating a Custom Theme
|
||||
|
||||
The easiest way to create a new theme is to use the `createTheme` function
|
||||
exported by the
|
||||
[`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme) package. You
|
||||
can use it to override some basic parameters of the default theme such as the
|
||||
color palette and font.
|
||||
The easiest way to create a new theme is to use the `createUnifiedTheme` function exported by the [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme) package. You can use it to override some basic parameters of the default theme such as the color palette and font.
|
||||
|
||||
For example, you can create a new theme based on the default light theme like
|
||||
this:
|
||||
For example, you can create a new theme based on the default light theme like this:
|
||||
|
||||
```ts
|
||||
import { createTheme, lightTheme } from '@backstage/theme';
|
||||
```ts title="packages/app/src/theme/myTheme.ts"
|
||||
import {
|
||||
createBaseThemeOptions,
|
||||
createUnifiedTheme,
|
||||
palettes,
|
||||
} from '@backstage/theme';
|
||||
|
||||
const myTheme = createTheme({
|
||||
palette: lightTheme.palette,
|
||||
const myTheme = createUnifiedTheme({
|
||||
...createBaseThemeOptions({
|
||||
palette: palettes.light,
|
||||
}),
|
||||
fontFamily: 'Comic Sans MS',
|
||||
defaultPageTheme: 'home',
|
||||
});
|
||||
```
|
||||
|
||||
If you want more control over the theme, and for example customize font sizes
|
||||
and margins, you can use the lower-level `createThemeOverrides` function
|
||||
exported by [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme)
|
||||
in combination with
|
||||
[`createTheme`](https://material-ui.com/customization/theming/#createmuitheme-options-args-theme)
|
||||
from [`@material-ui/core`](https://www.npmjs.com/package/@material-ui/core). See
|
||||
the "Overriding Backstage and Material UI css rules" section below.
|
||||
> Note: we recommend creating a `theme` folder in `packages/app/src` to place your theme file to keep things nicely organized.
|
||||
|
||||
You can also create a theme from scratch that matches the `BackstageTheme` type
|
||||
exported by [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme).
|
||||
See the
|
||||
[Material UI docs on theming](https://material-ui.com/customization/theming/)
|
||||
for more information about how that can be done.
|
||||
You can also create a theme from scratch that matches the `BackstageTheme` type exported by [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme). See the
|
||||
[Material UI docs on theming](https://material-ui.com/customization/theming/) for more information about how that can be done.
|
||||
|
||||
## Using your Custom Theme
|
||||
|
||||
To add a custom theme to your Backstage app, you pass it as configuration to
|
||||
`createApp`.
|
||||
To add a custom theme to your Backstage app, you pass it as configuration to `createApp`.
|
||||
|
||||
For example, adding the theme that we created in the previous section can be
|
||||
done like this:
|
||||
For example, adding the theme that we created in the previous section can be done like this:
|
||||
|
||||
```tsx
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
import { createApp } from '@backstage/app-defaults';
|
||||
import { ThemeProvider } from '@material-ui/core/styles';
|
||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
@@ -68,70 +54,68 @@ const app = createApp({
|
||||
variant: 'light',
|
||||
icon: <LightIcon />,
|
||||
Provider: ({ children }) => (
|
||||
<ThemeProvider theme={myTheme}>
|
||||
<CssBaseline>{children}</CssBaseline>
|
||||
</ThemeProvider>
|
||||
<UnifiedThemeProvider theme={myTheme} children={children} />
|
||||
),
|
||||
}]
|
||||
})
|
||||
```
|
||||
|
||||
Note that your list of custom themes overrides the default themes. If you still
|
||||
want to use the default themes, they are exported as `lightTheme` and
|
||||
`darkTheme` from
|
||||
[`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme).
|
||||
Note that your list of custom themes overrides the default themes. If you still want to use the default themes, they are exported as `themes.light` and `themes.light` from [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme).
|
||||
|
||||
## Example of a custom theme
|
||||
|
||||
```ts
|
||||
```ts title="packages/app/src/theme/myTheme.ts"
|
||||
import {
|
||||
createTheme,
|
||||
createBaseThemeOptions,
|
||||
createUnifiedTheme,
|
||||
genPageTheme,
|
||||
lightTheme,
|
||||
palettes,
|
||||
shapes,
|
||||
} from '@backstage/theme';
|
||||
|
||||
const myTheme = createTheme({
|
||||
palette: {
|
||||
...lightTheme.palette,
|
||||
primary: {
|
||||
main: '#343b58',
|
||||
const myTheme = createUnifiedTheme({
|
||||
...createBaseThemeOptions({
|
||||
palette: {
|
||||
...palettes.light,
|
||||
primary: {
|
||||
main: '#343b58',
|
||||
},
|
||||
secondary: {
|
||||
main: '#565a6e',
|
||||
},
|
||||
error: {
|
||||
main: '#8c4351',
|
||||
},
|
||||
warning: {
|
||||
main: '#8f5e15',
|
||||
},
|
||||
info: {
|
||||
main: '#34548a',
|
||||
},
|
||||
success: {
|
||||
main: '#485e30',
|
||||
},
|
||||
background: {
|
||||
default: '#d5d6db',
|
||||
paper: '#d5d6db',
|
||||
},
|
||||
banner: {
|
||||
info: '#34548a',
|
||||
error: '#8c4351',
|
||||
text: '#343b58',
|
||||
link: '#565a6e',
|
||||
},
|
||||
errorBackground: '#8c4351',
|
||||
warningBackground: '#8f5e15',
|
||||
infoBackground: '#343b58',
|
||||
navigation: {
|
||||
background: '#343b58',
|
||||
indicator: '#8f5e15',
|
||||
color: '#d5d6db',
|
||||
selectedColor: '#ffffff',
|
||||
},
|
||||
},
|
||||
secondary: {
|
||||
main: '#565a6e',
|
||||
},
|
||||
error: {
|
||||
main: '#8c4351',
|
||||
},
|
||||
warning: {
|
||||
main: '#8f5e15',
|
||||
},
|
||||
info: {
|
||||
main: '#34548a',
|
||||
},
|
||||
success: {
|
||||
main: '#485e30',
|
||||
},
|
||||
background: {
|
||||
default: '#d5d6db',
|
||||
paper: '#d5d6db',
|
||||
},
|
||||
banner: {
|
||||
info: '#34548a',
|
||||
error: '#8c4351',
|
||||
text: '#343b58',
|
||||
link: '#565a6e',
|
||||
},
|
||||
errorBackground: '#8c4351',
|
||||
warningBackground: '#8f5e15',
|
||||
infoBackground: '#343b58',
|
||||
navigation: {
|
||||
background: '#343b58',
|
||||
indicator: '#8f5e15',
|
||||
color: '#d5d6db',
|
||||
selectedColor: '#ffffff',
|
||||
},
|
||||
},
|
||||
}),
|
||||
defaultPageTheme: 'home',
|
||||
fontFamily: 'Comic Sans MS',
|
||||
/* below drives the header colors */
|
||||
@@ -161,16 +145,92 @@ const myTheme = createTheme({
|
||||
});
|
||||
```
|
||||
|
||||
For a more complete example of a custom theme including Backstage and
|
||||
Material UI component overrides, see the [Aperture
|
||||
theme](https://github.com/backstage/demo/blob/master/packages/app/src/theme/aperture.ts)
|
||||
from the [Backstage demo site](https://demo.backstage.io).
|
||||
For a more complete example of a custom theme including Backstage and Material UI component overrides, see the [Aperture theme](https://github.com/backstage/demo/blob/master/packages/app/src/theme/aperture.ts) from the [Backstage demo site](https://demo.backstage.io).
|
||||
|
||||
## Custom Typography
|
||||
|
||||
When creating a custom theme you can also customize various aspects of the default typography, here's an example using simplified theme:
|
||||
|
||||
```ts title="packages/app/src/theme/myTheme.ts"
|
||||
import {
|
||||
createBaseThemeOptions,
|
||||
createUnifiedTheme,
|
||||
palettes,
|
||||
} from '@backstage/theme';
|
||||
|
||||
const myTheme = createUnifiedTheme({
|
||||
...createBaseThemeOptions({
|
||||
palette: palettes.light,
|
||||
typography: {
|
||||
htmlFontSize: 16,
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
h1: {
|
||||
fontSize: 54,
|
||||
fontWeight: 700,
|
||||
marginBottom: 10,
|
||||
},
|
||||
h2: {
|
||||
fontSize: 40,
|
||||
fontWeight: 700,
|
||||
marginBottom: 8,
|
||||
},
|
||||
h3: {
|
||||
fontSize: 32,
|
||||
fontWeight: 700,
|
||||
marginBottom: 6,
|
||||
},
|
||||
h4: {
|
||||
fontWeight: 700,
|
||||
fontSize: 28,
|
||||
marginBottom: 6,
|
||||
},
|
||||
h5: {
|
||||
fontWeight: 700,
|
||||
fontSize: 24,
|
||||
marginBottom: 4,
|
||||
},
|
||||
h6: {
|
||||
fontWeight: 700,
|
||||
fontSize: 20,
|
||||
marginBottom: 2,
|
||||
},
|
||||
},
|
||||
defaultPageTheme: 'home',
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
If you wanted to only override a sub-set of the typography setting, for example just `h1` then you would do this:
|
||||
|
||||
```ts title="packages/app/src/theme/myTheme.ts"
|
||||
import {
|
||||
createBaseThemeOptions,
|
||||
createUnifiedTheme,
|
||||
defaultTypography,
|
||||
palettes,
|
||||
} from '@backstage/theme';
|
||||
|
||||
const myTheme = createUnifiedTheme({
|
||||
...createBaseThemeOptions({
|
||||
palette: palettes.light,
|
||||
typography: {
|
||||
...defaultTypography,
|
||||
htmlFontSize: 16,
|
||||
fontFamily: 'Roboto, sans-serif',
|
||||
h1: {
|
||||
fontSize: 72,
|
||||
fontWeight: 700,
|
||||
marginBottom: 10,
|
||||
},
|
||||
},
|
||||
defaultPageTheme: 'home',
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
## Overriding Backstage and Material UI components styles
|
||||
|
||||
When creating a custom theme you would be applying different values to
|
||||
component's css rules that use the theme object. For example, a Backstage
|
||||
component's styles might look like this:
|
||||
When creating a custom theme you would be applying different values to component's CSS rules that use the theme object. For example, a Backstage component's styles might look like this:
|
||||
|
||||
```tsx
|
||||
const useStyles = makeStyles<BackstageTheme>(
|
||||
@@ -185,83 +245,50 @@ const useStyles = makeStyles<BackstageTheme>(
|
||||
);
|
||||
```
|
||||
|
||||
Notice how the `padding` is getting its value from `theme.spacing`, that means
|
||||
that setting a value for spacing in your custom theme would affect this
|
||||
component padding property and the same goes for `backgroundImage` which uses
|
||||
`theme.page.backgroundImage`. However, the `boxShadow` property doesn't
|
||||
reference any value from the theme, that means that creating a custom theme
|
||||
wouldn't be enough to alter the `box-shadow` property or to add css rules that
|
||||
aren't already defined like a margin. For these cases you should also create an
|
||||
override.
|
||||
Notice how the `padding` is getting its value from `theme.spacing`, that means that setting a value for spacing in your custom theme would affect this component padding property and the same goes for `backgroundImage` which uses `theme.page.backgroundImage`. However, the `boxShadow` property doesn't reference any value from the theme, that means that creating a custom theme wouldn't be enough to alter the `box-shadow` property or to add css rules that aren't already defined like a margin. For these cases you should also create an override.
|
||||
|
||||
```tsx
|
||||
import { createApp } from '@backstage/core-app-api';
|
||||
import { BackstageTheme, lightTheme } from '@backstage/theme';
|
||||
/**
|
||||
* The `@backstage/core-components` package exposes this type that
|
||||
* contains all Backstage and `material-ui` components that can be
|
||||
* overridden along with the classes key those components use.
|
||||
*/
|
||||
import { BackstageOverrides } from '@backstage/core-components';
|
||||
Here's how you would do that:
|
||||
|
||||
export const createCustomThemeOverrides = (
|
||||
theme: BackstageTheme,
|
||||
): BackstageOverrides => {
|
||||
return {
|
||||
```ts title="packages/app/src/theme/myTheme.ts"
|
||||
import {
|
||||
createBaseThemeOptions,
|
||||
createUnifiedTheme,
|
||||
palettes,
|
||||
} from '@backstage/theme';
|
||||
|
||||
const myTheme = createUnifiedTheme({
|
||||
...createBaseThemeOptions({
|
||||
palette: palettes.light,
|
||||
}),
|
||||
fontFamily: 'Comic Sans MS',
|
||||
defaultPageTheme: 'home',
|
||||
components: {
|
||||
BackstageHeader: {
|
||||
header: {
|
||||
width: 'auto',
|
||||
margin: '20px',
|
||||
boxShadow: 'none',
|
||||
borderBottom: `4px solid ${theme.palette.primary.main}`,
|
||||
styleOverrides: {
|
||||
header: ({ theme }) => ({
|
||||
width: 'auto',
|
||||
margin: '20px',
|
||||
boxShadow: 'none',
|
||||
borderBottom: `4px solid ${theme.palette.primary.main}`,
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const customTheme: BackstageTheme = {
|
||||
...lightTheme,
|
||||
overrides: {
|
||||
// These are the overrides that Backstage applies to `material-ui` components
|
||||
...lightTheme.overrides,
|
||||
// These are your custom overrides, either to `material-ui` or Backstage components.
|
||||
...createCustomThemeOverrides(lightTheme),
|
||||
},
|
||||
};
|
||||
|
||||
const app = createApp({
|
||||
apis: ...,
|
||||
plugins: ...,
|
||||
themes: [{
|
||||
id: 'my-theme',
|
||||
title: 'My Custom Theme',
|
||||
variant: 'light',
|
||||
Provider: ({ children }) => (
|
||||
<ThemeProvider theme={customTheme}>
|
||||
<CssBaseline>{children}</CssBaseline>
|
||||
</ThemeProvider>
|
||||
),
|
||||
}]
|
||||
});
|
||||
```
|
||||
|
||||
## Custom Logo
|
||||
|
||||
In addition to a custom theme, you can also customize the logo displayed at the
|
||||
far top left of the site.
|
||||
In addition to a custom theme, you can also customize the logo displayed at the far top left of the site.
|
||||
|
||||
In your frontend app, locate `src/components/Root/` folder. You'll find two
|
||||
components:
|
||||
In your frontend app, locate `src/components/Root/` folder. You'll find two components:
|
||||
|
||||
- `LogoFull.tsx` - A larger logo used when the Sidebar navigation is opened.
|
||||
- `LogoIcon.tsx` - A smaller logo used when the sidebar navigation is closed.
|
||||
- `LogoIcon.tsx` - A smaller logo used when the Sidebar navigation is closed.
|
||||
|
||||
To replace the images, you can simply replace the relevant code in those
|
||||
components with raw SVG definitions.
|
||||
To replace the images, you can simply replace the relevant code in those components with raw SVG definitions.
|
||||
|
||||
You can also use another web image format such as PNG by importing it. To do
|
||||
this, place your new image into a new subdirectory such as
|
||||
`src/components/Root/logo/my-company-logo.png`, and then add this code:
|
||||
You can also use another web image format such as PNG by importing it. To do this, place your new image into a new subdirectory such as `src/components/Root/logo/my-company-logo.png`, and then add this code:
|
||||
|
||||
```tsx
|
||||
import MyCustomLogoFull from './logo/my-company-logo.png';
|
||||
@@ -408,7 +435,7 @@ For this example we'll show you how you can expand the sidebar with a sub-menu:
|
||||
|
||||
3. Then update the `@backstage/core-components` import like this:
|
||||
|
||||
```tsx
|
||||
```tsx title="packages/app/src/components/Root/Root.tsx"
|
||||
import {
|
||||
Sidebar,
|
||||
sidebarConfig,
|
||||
@@ -430,7 +457,7 @@ For this example we'll show you how you can expand the sidebar with a sub-menu:
|
||||
|
||||
4. Finally replace `<SidebarItem icon={HomeIcon} to="catalog" text="Home" />` with this:
|
||||
|
||||
```tsx
|
||||
```tsx title="packages/app/src/components/Root/Root.tsx"
|
||||
<SidebarItem icon={HomeIcon} to="catalog" text="Home">
|
||||
<SidebarSubmenu title="Catalog">
|
||||
<SidebarSubmenuItem
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -116,6 +116,7 @@
|
||||
"features/software-templates/input-examples",
|
||||
"features/software-templates/builtin-actions",
|
||||
"features/software-templates/writing-custom-actions",
|
||||
"features/software-templates/writing-tests-for-actions",
|
||||
"features/software-templates/writing-custom-field-extensions",
|
||||
"features/software-templates/writing-custom-step-layouts",
|
||||
"features/software-templates/authorizing-parameters-steps-and-actions",
|
||||
@@ -307,6 +308,7 @@
|
||||
"auth/gitlab/provider",
|
||||
"auth/google/provider",
|
||||
"auth/google/gcp-iap-auth",
|
||||
"auth/guest/provider",
|
||||
"auth/okta/provider",
|
||||
"auth/oauth2-proxy/provider",
|
||||
"auth/onelogin/provider",
|
||||
|
||||
@@ -161,6 +161,7 @@ nav:
|
||||
- GitLab: 'auth/gitlab/provider.md'
|
||||
- Google: 'auth/google/provider.md'
|
||||
- Google IAP: 'auth/google/gcp-iap-auth.md'
|
||||
- Guest: 'auth/guest/provider.md'
|
||||
- OAuth2Proxy: 'auth/oauth2-proxy/provider.md'
|
||||
- Okta: 'auth/okta/provider.md'
|
||||
- OneLogin: 'auth/onelogin/provider.md'
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "root",
|
||||
"version": "1.23.0",
|
||||
"version": "1.24.0-next.0",
|
||||
"private": true,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# @backstage/app-defaults
|
||||
|
||||
## 1.5.1-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/theme@0.5.2-next.0
|
||||
- @backstage/core-components@0.14.1-next.0
|
||||
- @backstage/core-plugin-api@1.9.1-next.0
|
||||
- @backstage/core-app-api@1.12.1-next.0
|
||||
- @backstage/plugin-permission-react@0.4.21-next.0
|
||||
|
||||
## 1.5.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/app-defaults",
|
||||
"version": "1.5.0",
|
||||
"version": "1.5.1-next.0",
|
||||
"description": "Provides the default wiring of a Backstage App",
|
||||
"backstage": {
|
||||
"role": "web-library"
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# app-next-example-plugin
|
||||
|
||||
## 0.0.7-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/core-components@0.14.1-next.0
|
||||
- @backstage/frontend-plugin-api@0.6.1-next.0
|
||||
|
||||
## 0.0.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-next-example-plugin",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7-next.0",
|
||||
"description": "Backstage internal example plugin",
|
||||
"backstage": {
|
||||
"role": "frontend-plugin"
|
||||
|
||||
@@ -1,5 +1,81 @@
|
||||
# example-app-next
|
||||
|
||||
## 0.0.7-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/plugin-adr@0.6.14-next.0
|
||||
- @backstage/theme@0.5.2-next.0
|
||||
- @backstage/core-components@0.14.1-next.0
|
||||
- @backstage/integration-react@1.1.25-next.0
|
||||
- @backstage/cli@0.25.3-next.0
|
||||
- @backstage/plugin-catalog-react@1.10.1-next.0
|
||||
- @backstage/plugin-azure-devops@0.4.0-next.0
|
||||
- @backstage/plugin-linguist@0.1.16-next.0
|
||||
- @backstage/plugin-catalog@1.17.1-next.0
|
||||
- @backstage/plugin-org@0.6.21-next.0
|
||||
- @backstage/plugin-search-common@1.2.11-next.0
|
||||
- @backstage/plugin-search@1.4.7-next.0
|
||||
- @backstage/plugin-devtools@0.1.10-next.0
|
||||
- @backstage/plugin-tech-radar@0.6.14-next.0
|
||||
- @backstage/plugin-scaffolder-react@1.8.1-next.0
|
||||
- @backstage/plugin-api-docs@0.11.1-next.0
|
||||
- @backstage/plugin-cost-insights@0.12.20-next.0
|
||||
- @backstage/plugin-home@0.6.3-next.0
|
||||
- @backstage/plugin-scaffolder@1.18.1-next.0
|
||||
- @backstage/plugin-shortcuts@0.3.20-next.0
|
||||
- @backstage/plugin-catalog-import@0.10.7-next.0
|
||||
- @backstage/catalog-model@1.4.5-next.0
|
||||
- @backstage/core-plugin-api@1.9.1-next.0
|
||||
- @backstage/frontend-app-api@0.6.1-next.0
|
||||
- @backstage/plugin-badges@0.2.55-next.0
|
||||
- @backstage/plugin-catalog-unprocessed-entities@0.1.9-next.0
|
||||
- @backstage/plugin-code-coverage@0.2.24-next.0
|
||||
- @backstage/plugin-entity-feedback@0.2.14-next.0
|
||||
- @backstage/plugin-explore@0.4.17-next.0
|
||||
- @backstage/plugin-gcalendar@0.3.24-next.0
|
||||
- @backstage/plugin-gocd@0.1.37-next.0
|
||||
- @backstage/plugin-jenkins@0.9.6-next.0
|
||||
- @backstage/plugin-microsoft-calendar@0.1.13-next.0
|
||||
- @backstage/plugin-newrelic-dashboard@0.3.6-next.0
|
||||
- @backstage/plugin-pagerduty@0.7.3-next.0
|
||||
- @backstage/plugin-playlist@0.2.5-next.0
|
||||
- @backstage/plugin-puppetdb@0.1.14-next.0
|
||||
- @backstage/plugin-stackstorm@0.1.12-next.0
|
||||
- @backstage/plugin-tech-insights@0.3.23-next.0
|
||||
- @backstage/plugin-techdocs@1.10.1-next.0
|
||||
- @backstage/plugin-todo@0.2.35-next.0
|
||||
- @backstage/plugin-user-settings@0.8.2-next.0
|
||||
- @backstage/app-defaults@1.5.1-next.0
|
||||
- @backstage/plugin-azure-sites@0.1.20-next.0
|
||||
- @backstage/plugin-search-react@1.7.7-next.0
|
||||
- @backstage/plugin-techdocs-react@1.1.17-next.0
|
||||
- app-next-example-plugin@0.0.7-next.0
|
||||
- @backstage/frontend-plugin-api@0.6.1-next.0
|
||||
- @backstage/plugin-airbrake@0.3.31-next.0
|
||||
- @backstage/plugin-apache-airflow@0.2.21-next.0
|
||||
- @backstage/plugin-app-visualizer@0.1.2-next.0
|
||||
- @backstage/plugin-catalog-graph@0.4.1-next.0
|
||||
- @backstage/plugin-cloudbuild@0.4.1-next.0
|
||||
- @backstage/plugin-dynatrace@9.0.1-next.0
|
||||
- @backstage/plugin-gcp-projects@0.3.47-next.0
|
||||
- @backstage/plugin-github-actions@0.6.12-next.0
|
||||
- @backstage/plugin-graphiql@0.3.4-next.0
|
||||
- @backstage/plugin-kafka@0.3.31-next.0
|
||||
- @backstage/plugin-kubernetes@0.11.6-next.0
|
||||
- @backstage/plugin-lighthouse@0.4.16-next.0
|
||||
- @backstage/plugin-newrelic@0.3.46-next.0
|
||||
- @backstage/plugin-octopus-deploy@0.2.13-next.0
|
||||
- @backstage/plugin-rollbar@0.4.31-next.0
|
||||
- @backstage/plugin-sentry@0.5.16-next.0
|
||||
- @backstage/plugin-techdocs-module-addons-contrib@1.1.6-next.0
|
||||
- @backstage/core-app-api@1.12.1-next.0
|
||||
- @backstage/core-compat-api@0.2.1-next.0
|
||||
- @backstage/plugin-catalog-common@1.0.22-next.0
|
||||
- @backstage/plugin-linguist-common@0.1.2
|
||||
- @backstage/plugin-permission-react@0.4.21-next.0
|
||||
|
||||
## 0.0.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-app-next",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7-next.0",
|
||||
"private": true,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,5 +1,84 @@
|
||||
# example-app
|
||||
|
||||
## 0.2.93-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/plugin-github-pull-requests-board@0.1.25-next.0
|
||||
- @backstage/plugin-adr@0.6.14-next.0
|
||||
- @backstage/plugin-stack-overflow@0.1.26-next.0
|
||||
- @backstage/theme@0.5.2-next.0
|
||||
- @backstage/core-components@0.14.1-next.0
|
||||
- @backstage/plugin-notifications@0.1.0-next.0
|
||||
- @backstage/integration-react@1.1.25-next.0
|
||||
- @backstage/cli@0.25.3-next.0
|
||||
- @backstage/plugin-catalog-react@1.10.1-next.0
|
||||
- @backstage/plugin-azure-devops@0.4.0-next.0
|
||||
- @backstage/plugin-linguist@0.1.16-next.0
|
||||
- @backstage/plugin-catalog@1.17.1-next.0
|
||||
- @backstage/plugin-org@0.6.21-next.0
|
||||
- @backstage/plugin-search-common@1.2.11-next.0
|
||||
- @backstage/plugin-search@1.4.7-next.0
|
||||
- @backstage/plugin-devtools@0.1.10-next.0
|
||||
- @backstage/plugin-tech-radar@0.6.14-next.0
|
||||
- @backstage/plugin-scaffolder-react@1.8.1-next.0
|
||||
- @backstage/plugin-api-docs@0.11.1-next.0
|
||||
- @backstage/plugin-cost-insights@0.12.20-next.0
|
||||
- @backstage/plugin-home@0.6.3-next.0
|
||||
- @backstage/plugin-scaffolder@1.18.1-next.0
|
||||
- @backstage/plugin-shortcuts@0.3.20-next.0
|
||||
- @backstage/plugin-signals@0.0.2-next.0
|
||||
- @backstage/plugin-catalog-import@0.10.7-next.0
|
||||
- @backstage/catalog-model@1.4.5-next.0
|
||||
- @backstage/config@1.1.2-next.0
|
||||
- @backstage/core-plugin-api@1.9.1-next.0
|
||||
- @backstage/frontend-app-api@0.6.1-next.0
|
||||
- @backstage/plugin-badges@0.2.55-next.0
|
||||
- @backstage/plugin-catalog-unprocessed-entities@0.1.9-next.0
|
||||
- @backstage/plugin-code-coverage@0.2.24-next.0
|
||||
- @backstage/plugin-entity-feedback@0.2.14-next.0
|
||||
- @backstage/plugin-explore@0.4.17-next.0
|
||||
- @backstage/plugin-gcalendar@0.3.24-next.0
|
||||
- @backstage/plugin-gocd@0.1.37-next.0
|
||||
- @backstage/plugin-jenkins@0.9.6-next.0
|
||||
- @backstage/plugin-microsoft-calendar@0.1.13-next.0
|
||||
- @backstage/plugin-newrelic-dashboard@0.3.6-next.0
|
||||
- @backstage/plugin-pagerduty@0.7.3-next.0
|
||||
- @backstage/plugin-playlist@0.2.5-next.0
|
||||
- @backstage/plugin-puppetdb@0.1.14-next.0
|
||||
- @backstage/plugin-stackstorm@0.1.12-next.0
|
||||
- @backstage/plugin-tech-insights@0.3.23-next.0
|
||||
- @backstage/plugin-techdocs@1.10.1-next.0
|
||||
- @backstage/plugin-todo@0.2.35-next.0
|
||||
- @backstage/plugin-user-settings@0.8.2-next.0
|
||||
- @backstage/app-defaults@1.5.1-next.0
|
||||
- @backstage/plugin-azure-sites@0.1.20-next.0
|
||||
- @backstage/plugin-search-react@1.7.7-next.0
|
||||
- @backstage/plugin-techdocs-react@1.1.17-next.0
|
||||
- @backstage/plugin-airbrake@0.3.31-next.0
|
||||
- @backstage/plugin-apache-airflow@0.2.21-next.0
|
||||
- @backstage/plugin-catalog-graph@0.4.1-next.0
|
||||
- @backstage/plugin-cloudbuild@0.4.1-next.0
|
||||
- @backstage/plugin-dynatrace@9.0.1-next.0
|
||||
- @backstage/plugin-gcp-projects@0.3.47-next.0
|
||||
- @backstage/plugin-github-actions@0.6.12-next.0
|
||||
- @backstage/plugin-graphiql@0.3.4-next.0
|
||||
- @backstage/plugin-kafka@0.3.31-next.0
|
||||
- @backstage/plugin-kubernetes@0.11.6-next.0
|
||||
- @backstage/plugin-kubernetes-cluster@0.0.7-next.0
|
||||
- @backstage/plugin-lighthouse@0.4.16-next.0
|
||||
- @backstage/plugin-newrelic@0.3.46-next.0
|
||||
- @backstage/plugin-nomad@0.1.12-next.0
|
||||
- @backstage/plugin-octopus-deploy@0.2.13-next.0
|
||||
- @backstage/plugin-rollbar@0.4.31-next.0
|
||||
- @backstage/plugin-sentry@0.5.16-next.0
|
||||
- @backstage/plugin-techdocs-module-addons-contrib@1.1.6-next.0
|
||||
- @backstage/core-app-api@1.12.1-next.0
|
||||
- @backstage/plugin-catalog-common@1.0.22-next.0
|
||||
- @backstage/plugin-linguist-common@0.1.2
|
||||
- @backstage/plugin-permission-react@0.4.21-next.0
|
||||
|
||||
## 0.2.92
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-app",
|
||||
"version": "0.2.92",
|
||||
"version": "0.2.93-next.0",
|
||||
"private": true,
|
||||
"backstage": {
|
||||
"role": "frontend"
|
||||
|
||||
@@ -87,11 +87,6 @@ const SearchPage = () => {
|
||||
name: 'Documentation',
|
||||
icon: <DocsIcon />,
|
||||
},
|
||||
{
|
||||
value: 'adr',
|
||||
name: 'Architecture Decision Records',
|
||||
icon: <DocsIcon />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Paper className={classes.filters}>
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# @backstage/backend-app-api
|
||||
|
||||
## 0.6.0-next.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 4a3d434: **BREAKING**: For users that have migrated to the new backend system, incoming requests will now be rejected if they are not properly authenticated (e.g. with a Backstage bearer token or a backend token). Please see the [Auth Service Migration tutorial](https://backstage.io/docs/tutorials/auth-service-migration) for more information on how to circumvent this behavior in the short term and how to properly leverage it in the longer term.
|
||||
|
||||
Added service factories for the new [`auth`](https://backstage.io/docs/backend-system/core-services/auth/), [`httpAuth`](https://backstage.io/docs/backend-system/core-services/http-auth), and [`userInfo`](https://backstage.io/docs/backend-system/core-services/user-info) services that were created as part of [BEP-0003](https://github.com/backstage/backstage/tree/master/beps/0003-auth-architecture-evolution).
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 999224f: Bump dependency `minimatch` to v9
|
||||
- 0502d82: Updated the `permissionsServiceFactory` to forward the `AuthService` to the implementation.
|
||||
- 9802004: Made the `DefaultUserInfoService` claims check stricter
|
||||
- Updated dependencies
|
||||
- @backstage/backend-common@0.21.3-next.0
|
||||
- @backstage/plugin-auth-node@0.4.8-next.0
|
||||
- @backstage/errors@1.2.4-next.0
|
||||
- @backstage/backend-plugin-api@0.6.13-next.0
|
||||
- @backstage/backend-tasks@0.5.18-next.0
|
||||
- @backstage/plugin-permission-node@0.7.24-next.0
|
||||
- @backstage/cli-node@0.2.4-next.0
|
||||
- @backstage/config-loader@1.6.3-next.0
|
||||
- @backstage/config@1.1.2-next.0
|
||||
- @backstage/cli-common@0.1.13
|
||||
- @backstage/types@1.1.1
|
||||
|
||||
## 0.5.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/backend-app-api",
|
||||
"description": "Core API used by Backstage backend apps",
|
||||
"version": "0.5.11",
|
||||
"version": "0.6.0-next.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -28,10 +28,12 @@ import {
|
||||
BackstageServicePrincipal,
|
||||
BackstageUserPrincipal,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { tokenManagerServiceFactory } from '../tokenManager';
|
||||
|
||||
// TODO: Ship discovery mock service in the service factory tester
|
||||
const mockDeps = [
|
||||
discoveryServiceFactory(),
|
||||
tokenManagerServiceFactory,
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ServerTokenManager, TokenManager } from '@backstage/backend-common';
|
||||
import { TokenManager } from '@backstage/backend-common';
|
||||
import {
|
||||
AuthService,
|
||||
BackstageCredentials,
|
||||
@@ -27,10 +27,7 @@ import {
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
import {
|
||||
DefaultIdentityClient,
|
||||
IdentityApiGetIdentityRequest,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import { IdentityApiGetIdentityRequest } from '@backstage/plugin-auth-node';
|
||||
import { decodeJwt } from 'jose';
|
||||
|
||||
/** @internal */
|
||||
@@ -38,37 +35,37 @@ export type InternalBackstageCredentials<TPrincipal = unknown> =
|
||||
BackstageCredentials<TPrincipal> & {
|
||||
version: string;
|
||||
token?: string;
|
||||
authMethod: 'token' | 'cookie' | 'none';
|
||||
};
|
||||
|
||||
export function createCredentialsWithServicePrincipal(
|
||||
sub: string,
|
||||
token?: string,
|
||||
): InternalBackstageCredentials<BackstageServicePrincipal> {
|
||||
return {
|
||||
$$type: '@backstage/BackstageCredentials',
|
||||
version: 'v1',
|
||||
token,
|
||||
principal: {
|
||||
type: 'service',
|
||||
subject: sub,
|
||||
},
|
||||
authMethod: 'token',
|
||||
};
|
||||
}
|
||||
|
||||
export function createCredentialsWithUserPrincipal(
|
||||
sub: string,
|
||||
token: string,
|
||||
authMethod: 'token' | 'cookie' = 'token',
|
||||
expiresAt?: Date,
|
||||
): InternalBackstageCredentials<BackstageUserPrincipal> {
|
||||
return {
|
||||
$$type: '@backstage/BackstageCredentials',
|
||||
version: 'v1',
|
||||
token,
|
||||
expiresAt,
|
||||
principal: {
|
||||
type: 'user',
|
||||
userEntityRef: sub,
|
||||
},
|
||||
authMethod,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -79,7 +76,6 @@ export function createCredentialsWithNonePrincipal(): InternalBackstageCredentia
|
||||
principal: {
|
||||
type: 'none',
|
||||
},
|
||||
authMethod: 'none',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -114,6 +110,7 @@ class DefaultAuthService implements AuthService {
|
||||
private readonly disableDefaultAuthPolicy: boolean,
|
||||
) {}
|
||||
|
||||
// allowLimitedAccess is currently ignored, since we currently always use the full user tokens
|
||||
async authenticate(token: string): Promise<BackstageCredentials> {
|
||||
const { sub, aud } = decodeJwt(token);
|
||||
|
||||
@@ -137,6 +134,7 @@ class DefaultAuthService implements AuthService {
|
||||
return createCredentialsWithUserPrincipal(
|
||||
identity.identity.userEntityRef,
|
||||
token,
|
||||
this.#getJwtExpiration(token),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -159,6 +157,12 @@ class DefaultAuthService implements AuthService {
|
||||
return true;
|
||||
}
|
||||
|
||||
async getNoneCredentials(): Promise<
|
||||
BackstageCredentials<BackstageNonePrincipal>
|
||||
> {
|
||||
return createCredentialsWithNonePrincipal();
|
||||
}
|
||||
|
||||
async getOwnServiceCredentials(): Promise<
|
||||
BackstageCredentials<BackstageServicePrincipal>
|
||||
> {
|
||||
@@ -196,6 +200,30 @@ class DefaultAuthService implements AuthService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getLimitedUserToken(
|
||||
credentials: BackstageCredentials<BackstageUserPrincipal>,
|
||||
): Promise<{ token: string; expiresAt: Date }> {
|
||||
const internalCredentials = toInternalBackstageCredentials(credentials);
|
||||
|
||||
const { token } = internalCredentials;
|
||||
|
||||
if (!token) {
|
||||
throw new AuthenticationError(
|
||||
'User credentials is unexpectedly missing token',
|
||||
);
|
||||
}
|
||||
|
||||
return { token, expiresAt: this.#getJwtExpiration(token) };
|
||||
}
|
||||
|
||||
#getJwtExpiration(token: string) {
|
||||
const { exp } = decodeJwt(token);
|
||||
if (!exp) {
|
||||
throw new AuthenticationError('User token is missing expiration');
|
||||
}
|
||||
return new Date(exp * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
@@ -204,14 +232,15 @@ export const authServiceFactory = createServiceFactory({
|
||||
deps: {
|
||||
config: coreServices.rootConfig,
|
||||
logger: coreServices.rootLogger,
|
||||
discovery: coreServices.discovery,
|
||||
plugin: coreServices.pluginMetadata,
|
||||
identity: coreServices.identity,
|
||||
// Re-using the token manager makes sure that we use the same generated keys for
|
||||
// development as plugins that have not yet been migrated. It's important that this
|
||||
// keeps working as long as there are plugins that have not been migrated to the
|
||||
// new auth services in the new backend system.
|
||||
tokenManager: coreServices.tokenManager,
|
||||
},
|
||||
createRootContext({ config, logger }) {
|
||||
return ServerTokenManager.fromConfig(config, { logger });
|
||||
},
|
||||
async factory({ discovery, config, plugin }, tokenManager) {
|
||||
const identity = DefaultIdentityClient.create({ discovery });
|
||||
async factory({ config, plugin, identity, tokenManager }) {
|
||||
const disableDefaultAuthPolicy = Boolean(
|
||||
config.getOptionalBoolean(
|
||||
'backend.auth.dangerouslyDisableDefaultAuthPolicy',
|
||||
|
||||
+141
-61
@@ -18,6 +18,7 @@ import {
|
||||
AuthService,
|
||||
BackstageCredentials,
|
||||
BackstagePrincipalTypes,
|
||||
BackstageUserPrincipal,
|
||||
DiscoveryService,
|
||||
HttpAuthService,
|
||||
coreServices,
|
||||
@@ -26,11 +27,8 @@ import {
|
||||
import { AuthenticationError, NotAllowedError } from '@backstage/errors';
|
||||
import { parse as parseCookie } from 'cookie';
|
||||
import { Request, Response } from 'express';
|
||||
import { decodeJwt } from 'jose';
|
||||
import {
|
||||
createCredentialsWithNonePrincipal,
|
||||
toInternalBackstageCredentials,
|
||||
} from '../auth/authServiceFactory';
|
||||
|
||||
const FIVE_MINUTES_MS = 5 * 60 * 1000;
|
||||
|
||||
const BACKSTAGE_AUTH_COOKIE = 'backstage-auth';
|
||||
|
||||
@@ -41,54 +39,78 @@ function getTokenFromRequest(req: Request) {
|
||||
const matches = authHeader.match(/^Bearer[ ]+(\S+)$/i);
|
||||
const token = matches?.[1];
|
||||
if (token) {
|
||||
return { token, isCookie: false };
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getCookieFromRequest(req: Request) {
|
||||
const cookieHeader = req.headers.cookie;
|
||||
if (cookieHeader) {
|
||||
const cookies = parseCookie(cookieHeader);
|
||||
const token = cookies[BACKSTAGE_AUTH_COOKIE];
|
||||
if (token) {
|
||||
return { token, isCookie: true };
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
return { token: undefined, isCookie: false };
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function willExpireSoon(expiresAt: Date) {
|
||||
return Date.now() + FIVE_MINUTES_MS > expiresAt.getTime();
|
||||
}
|
||||
|
||||
const credentialsSymbol = Symbol('backstage-credentials');
|
||||
const limitedCredentialsSymbol = Symbol('backstage-limited-credentials');
|
||||
|
||||
type RequestWithCredentials = Request & {
|
||||
[credentialsSymbol]?: Promise<BackstageCredentials>;
|
||||
[limitedCredentialsSymbol]?: Promise<BackstageCredentials>;
|
||||
};
|
||||
|
||||
class DefaultHttpAuthService implements HttpAuthService {
|
||||
readonly #auth: AuthService;
|
||||
readonly #discovery: DiscoveryService;
|
||||
readonly #pluginId: string;
|
||||
|
||||
constructor(
|
||||
private readonly auth: AuthService,
|
||||
private readonly discovery: DiscoveryService,
|
||||
private readonly pluginId: string,
|
||||
) {}
|
||||
auth: AuthService,
|
||||
discovery: DiscoveryService,
|
||||
pluginId: string,
|
||||
) {
|
||||
this.#auth = auth;
|
||||
this.#discovery = discovery;
|
||||
this.#pluginId = pluginId;
|
||||
}
|
||||
|
||||
async #extractCredentialsFromRequest(req: Request) {
|
||||
const { token, isCookie } = getTokenFromRequest(req);
|
||||
const token = getTokenFromRequest(req);
|
||||
if (!token) {
|
||||
return createCredentialsWithNonePrincipal();
|
||||
return await this.#auth.getNoneCredentials();
|
||||
}
|
||||
|
||||
const credentials = toInternalBackstageCredentials(
|
||||
await this.auth.authenticate(token),
|
||||
);
|
||||
if (isCookie) {
|
||||
if (credentials.principal.type !== 'user') {
|
||||
throw new AuthenticationError(
|
||||
'Refusing to authenticate non-user principal with cookie auth',
|
||||
);
|
||||
}
|
||||
credentials.authMethod = 'cookie';
|
||||
return await this.#auth.authenticate(token);
|
||||
}
|
||||
|
||||
async #extractLimitedCredentialsFromRequest(req: Request) {
|
||||
const token = getTokenFromRequest(req);
|
||||
if (token) {
|
||||
return await this.#auth.authenticate(token, {
|
||||
allowLimitedAccess: true,
|
||||
});
|
||||
}
|
||||
|
||||
return credentials;
|
||||
const cookie = getCookieFromRequest(req);
|
||||
if (cookie) {
|
||||
return await this.#auth.authenticate(cookie, {
|
||||
allowLimitedAccess: true,
|
||||
});
|
||||
}
|
||||
|
||||
return await this.#auth.getNoneCredentials();
|
||||
}
|
||||
|
||||
async #getCredentials(req: RequestWithCredentials) {
|
||||
@@ -96,73 +118,131 @@ class DefaultHttpAuthService implements HttpAuthService {
|
||||
this.#extractCredentialsFromRequest(req));
|
||||
}
|
||||
|
||||
async #getLimitedCredentials(req: RequestWithCredentials) {
|
||||
return (req[limitedCredentialsSymbol] ??=
|
||||
this.#extractLimitedCredentialsFromRequest(req));
|
||||
}
|
||||
|
||||
async credentials<TAllowed extends keyof BackstagePrincipalTypes = 'unknown'>(
|
||||
req: Request,
|
||||
options?: {
|
||||
allow?: Array<TAllowed>;
|
||||
allowedAuthMethods?: Array<'token' | 'cookie'>;
|
||||
allowLimitedAccess?: boolean;
|
||||
},
|
||||
): Promise<BackstageCredentials<BackstagePrincipalTypes[TAllowed]>> {
|
||||
const credentials = toInternalBackstageCredentials(
|
||||
await this.#getCredentials(req),
|
||||
);
|
||||
// Limited and full credentials are treated as two separate cases, this lets
|
||||
// us avoid internal dependencies between the AuthService and
|
||||
// HttpAuthService implementations
|
||||
const credentials = options?.allowLimitedAccess
|
||||
? await this.#getLimitedCredentials(req)
|
||||
: await this.#getCredentials(req);
|
||||
|
||||
const allowedPrincipalTypes = options?.allow;
|
||||
const allowedAuthMethods: Array<'token' | 'cookie' | 'none'> =
|
||||
options?.allowedAuthMethods ?? ['token'];
|
||||
|
||||
if (
|
||||
credentials.authMethod !== 'none' &&
|
||||
!allowedAuthMethods.includes(credentials.authMethod)
|
||||
) {
|
||||
throw new NotAllowedError(
|
||||
`This endpoint does not allow the '${credentials.authMethod}' auth method`,
|
||||
);
|
||||
const allowed = options?.allow;
|
||||
if (!allowed) {
|
||||
return credentials as any;
|
||||
}
|
||||
|
||||
if (
|
||||
allowedPrincipalTypes &&
|
||||
!allowedPrincipalTypes.includes(credentials.principal.type as TAllowed)
|
||||
) {
|
||||
if (credentials.authMethod === 'none') {
|
||||
throw new AuthenticationError();
|
||||
if (this.#auth.isPrincipal(credentials, 'none')) {
|
||||
if (allowed.includes('none' as TAllowed)) {
|
||||
return credentials as any;
|
||||
}
|
||||
|
||||
throw new AuthenticationError('Missing credentials');
|
||||
} else if (this.#auth.isPrincipal(credentials, 'user')) {
|
||||
if (allowed.includes('user' as TAllowed)) {
|
||||
return credentials as any;
|
||||
}
|
||||
|
||||
throw new NotAllowedError(
|
||||
`This endpoint does not allow '${credentials.principal.type}' credentials`,
|
||||
`This endpoint does not allow 'user' credentials`,
|
||||
);
|
||||
} else if (this.#auth.isPrincipal(credentials, 'service')) {
|
||||
if (allowed.includes('service' as TAllowed)) {
|
||||
return credentials as any;
|
||||
}
|
||||
|
||||
throw new NotAllowedError(
|
||||
`This endpoint does not allow 'service' credentials`,
|
||||
);
|
||||
}
|
||||
|
||||
return credentials as any;
|
||||
throw new NotAllowedError(
|
||||
'Unknown principal type, this should never happen',
|
||||
);
|
||||
}
|
||||
|
||||
async issueUserCookie(res: Response): Promise<void> {
|
||||
const credentials = await this.credentials(res.req, { allow: ['user'] });
|
||||
async issueUserCookie(
|
||||
res: Response,
|
||||
options?: { credentials?: BackstageCredentials },
|
||||
): Promise<{ expiresAt: Date }> {
|
||||
if (res.headersSent) {
|
||||
throw new Error('Failed to issue user cookie, headers were already sent');
|
||||
}
|
||||
|
||||
let credentials: BackstageCredentials<BackstageUserPrincipal>;
|
||||
if (options?.credentials) {
|
||||
if (!this.#auth.isPrincipal(options.credentials, 'user')) {
|
||||
throw new AuthenticationError(
|
||||
'Refused to issue cookie for non-user principal',
|
||||
);
|
||||
}
|
||||
credentials = options.credentials;
|
||||
} else {
|
||||
credentials = await this.credentials(res.req, { allow: ['user'] });
|
||||
}
|
||||
|
||||
const existingExpiresAt = await this.#existingCookieExpiration(res.req);
|
||||
if (existingExpiresAt && !willExpireSoon(existingExpiresAt)) {
|
||||
return { expiresAt: existingExpiresAt };
|
||||
}
|
||||
|
||||
const originHeader = res.req.headers.origin;
|
||||
const origin =
|
||||
!originHeader || originHeader === 'null' ? undefined : originHeader;
|
||||
|
||||
// https://backstage.example.com/api/catalog
|
||||
const externalBaseUrlStr = await this.discovery.getExternalBaseUrl(
|
||||
this.pluginId,
|
||||
const externalBaseUrlStr = await this.#discovery.getExternalBaseUrl(
|
||||
this.#pluginId,
|
||||
);
|
||||
const externalBaseUrl = new URL(externalBaseUrlStr);
|
||||
const externalBaseUrl = new URL(origin ?? externalBaseUrlStr);
|
||||
|
||||
const { token } = toInternalBackstageCredentials(credentials);
|
||||
const { token, expiresAt } = await this.#auth.getLimitedUserToken(
|
||||
credentials,
|
||||
);
|
||||
if (!token) {
|
||||
throw new Error('User credentials is unexpectedly missing token');
|
||||
}
|
||||
|
||||
// TODO: Proper refresh and expiration handling
|
||||
const expires = decodeJwt(token).exp!;
|
||||
const secure =
|
||||
externalBaseUrl.protocol === 'https:' ||
|
||||
externalBaseUrl.hostname === 'localhost';
|
||||
|
||||
// TODO: refresh this thing
|
||||
res.cookie(BACKSTAGE_AUTH_COOKIE, token, {
|
||||
domain: externalBaseUrl.hostname,
|
||||
httpOnly: true,
|
||||
expires: new Date(expires * 1000),
|
||||
path: externalBaseUrl.pathname,
|
||||
expires: expiresAt,
|
||||
secure,
|
||||
priority: 'high',
|
||||
sameSite: 'lax', // TBD
|
||||
sameSite: secure ? 'none' : 'lax',
|
||||
});
|
||||
|
||||
throw new Error('Method not implemented.');
|
||||
return { expiresAt };
|
||||
}
|
||||
|
||||
async #existingCookieExpiration(req: Request): Promise<Date | undefined> {
|
||||
const existingCookie = getCookieFromRequest(req);
|
||||
if (!existingCookie) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const existingCredentials = await this.#auth.authenticate(existingCookie, {
|
||||
allowLimitedAccess: true,
|
||||
});
|
||||
if (!this.#auth.isPrincipal(existingCredentials, 'user')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return existingCredentials.expiresAt;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+56
-3
@@ -53,7 +53,10 @@ describe('createCredentialsBarrier', () => {
|
||||
.expect(401)
|
||||
.expect(res =>
|
||||
expect(res.body).toMatchObject({
|
||||
error: { name: 'AuthenticationError', message: '' },
|
||||
error: {
|
||||
name: 'AuthenticationError',
|
||||
message: 'Missing credentials',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -98,7 +101,7 @@ describe('createCredentialsBarrier', () => {
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('should allow exceptions to the default auth policy to be made', async () => {
|
||||
it('should allow exceptions for unauthenticated access', async () => {
|
||||
const { app, barrier } = setup();
|
||||
|
||||
await request(app).get('/').send().expect(401);
|
||||
@@ -118,5 +121,55 @@ describe('createCredentialsBarrier', () => {
|
||||
await request(app).get('/other').send().expect(200);
|
||||
});
|
||||
|
||||
// TODO: cookie auth
|
||||
it('should allow exceptions for cookie access', async () => {
|
||||
const { app, barrier } = setup();
|
||||
|
||||
await request(app).get('/').send().expect(401);
|
||||
await request(app).get('/public').send().expect(401);
|
||||
await request(app).get('/other').send().expect(401);
|
||||
await request(app)
|
||||
.get('/static')
|
||||
.set('cookie', mockCredentials.limitedUser.cookie())
|
||||
.send()
|
||||
.expect(401);
|
||||
await request(app)
|
||||
.get('/static')
|
||||
.set('authorization', mockCredentials.user.header())
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
barrier.addAuthPolicy({ allow: 'user-cookie', path: '/static' });
|
||||
|
||||
await request(app).get('/').send().expect(401);
|
||||
await request(app).get('/static').send().expect(401);
|
||||
await request(app)
|
||||
.get('/static')
|
||||
.set('cookie', mockCredentials.limitedUser.cookie())
|
||||
.send()
|
||||
.expect(200);
|
||||
await request(app)
|
||||
.get('/static')
|
||||
.set('authorization', mockCredentials.user.header())
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
await request(app).get('/other').send().expect(401);
|
||||
|
||||
// Unauthenticated access should take precedence
|
||||
barrier.addAuthPolicy({ allow: 'unauthenticated', path: '/' });
|
||||
|
||||
await request(app).get('/').send().expect(200);
|
||||
await request(app).get('/static').send().expect(200);
|
||||
await request(app)
|
||||
.get('/static')
|
||||
.set('cookie', mockCredentials.limitedUser.cookie())
|
||||
.send()
|
||||
.expect(200);
|
||||
await request(app)
|
||||
.get('/static')
|
||||
.set('cookie', mockCredentials.limitedUser.invalidCookie())
|
||||
.send()
|
||||
.expect(200);
|
||||
await request(app).get('/other').send().expect(200);
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ export function createCredentialsBarrier(options: {
|
||||
httpAuth
|
||||
.credentials(req, {
|
||||
allow: ['user', 'service'],
|
||||
allowedAuthMethods: allowsCookie ? ['token', 'cookie'] : ['token'],
|
||||
allowLimitedAccess: allowsCookie,
|
||||
})
|
||||
.then(
|
||||
() => next(),
|
||||
|
||||
+5
-2
@@ -43,8 +43,11 @@ export class DefaultUserInfoService implements UserInfoService {
|
||||
if (typeof userEntityRef !== 'string') {
|
||||
throw new Error('User entity ref must be a string');
|
||||
}
|
||||
if (!Array.isArray(ownershipEntityRefs)) {
|
||||
throw new Error('Ownership entity refs must be an array');
|
||||
if (
|
||||
!Array.isArray(ownershipEntityRefs) ||
|
||||
ownershipEntityRefs.some(ref => typeof ref !== 'string')
|
||||
) {
|
||||
throw new Error('Ownership entity refs must be an array of strings');
|
||||
}
|
||||
|
||||
return { userEntityRef, ownershipEntityRefs };
|
||||
|
||||
@@ -1,5 +1,35 @@
|
||||
# @backstage/backend-common
|
||||
|
||||
## 0.21.3-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7422430: Resolve the `basePath` before constructing the target path
|
||||
- 999224f: Bump dependency `minimatch` to v9
|
||||
- e0b997c: Fix issue where `resolveSafeChildPath` path would incorrectly resolve when operating on a symlink
|
||||
- 9802004: Added the `UserInfoApi` as both an optional input and as an output for `createLegacyAuthAdapters`
|
||||
- 2af5354: Bump dependency `jose` to v5
|
||||
- ff40ada: Updated dependency `mysql2` to `^3.0.0`.
|
||||
- 0fb419b: Updated dependency `uuid` to `^9.0.0`.
|
||||
Updated dependency `@types/uuid` to `^9.0.0`.
|
||||
- 568881f: Updated dependency `yauzl` to `^3.0.0`.
|
||||
- 4a3d434: Added a `createLegacyAuthAdapters` function that can be used as a compatibility adapter for backend plugins who want to start using the new [`auth`](https://backstage.io/docs/backend-system/core-services/auth/) and [`httpAuth`](https://backstage.io/docs/backend-system/core-services/http-auth) services that were created as part of [BEP-0003](https://github.com/backstage/backstage/tree/master/beps/0003-auth-architecture-evolution).
|
||||
|
||||
See the [Auth Service Migration tutorial](https://backstage.io/docs/tutorials/auth-service-migration) for more information on the usage of this adapter.
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/plugin-auth-node@0.4.8-next.0
|
||||
- @backstage/errors@1.2.4-next.0
|
||||
- @backstage/backend-plugin-api@0.6.13-next.0
|
||||
- @backstage/backend-app-api@0.6.0-next.0
|
||||
- @backstage/config-loader@1.6.3-next.0
|
||||
- @backstage/config@1.1.2-next.0
|
||||
- @backstage/integration@1.9.1-next.0
|
||||
- @backstage/integration-aws-node@0.1.10-next.0
|
||||
- @backstage/backend-dev-utils@0.1.4
|
||||
- @backstage/cli-common@0.1.13
|
||||
- @backstage/types@1.1.1
|
||||
|
||||
## 0.21.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -67,6 +67,7 @@ import { ServiceRef } from '@backstage/backend-plugin-api';
|
||||
import { TokenManagerService as TokenManager } from '@backstage/backend-plugin-api';
|
||||
import { TransportStreamOptions } from 'winston-transport';
|
||||
import { UrlReaderService as UrlReader } from '@backstage/backend-plugin-api';
|
||||
import { UserInfoService } from '@backstage/backend-plugin-api';
|
||||
import { V1PodTemplateSpec } from '@kubernetes/client-node';
|
||||
import * as winston from 'winston';
|
||||
import { Writable } from 'stream';
|
||||
@@ -239,30 +240,32 @@ export function createLegacyAuthAdapters<
|
||||
TOptions extends {
|
||||
auth?: AuthService;
|
||||
httpAuth?: HttpAuthService;
|
||||
userInfo?: UserInfoService;
|
||||
identity?: IdentityService;
|
||||
tokenManager?: TokenManager;
|
||||
discovery: PluginEndpointDiscovery;
|
||||
},
|
||||
TAdapters = TOptions extends {
|
||||
TAdapters = (TOptions extends {
|
||||
auth?: AuthService;
|
||||
}
|
||||
? TOptions extends {
|
||||
httpAuth?: HttpAuthService;
|
||||
? {
|
||||
auth: AuthService;
|
||||
}
|
||||
: {}) &
|
||||
(TOptions extends {
|
||||
httpAuth?: HttpAuthService;
|
||||
}
|
||||
? {
|
||||
auth: AuthService;
|
||||
httpAuth: HttpAuthService;
|
||||
}
|
||||
: {
|
||||
auth: AuthService;
|
||||
: {}) &
|
||||
(TOptions extends {
|
||||
userInfo?: UserInfoService;
|
||||
}
|
||||
? {
|
||||
userInfo: UserInfoService;
|
||||
}
|
||||
: TOptions extends {
|
||||
httpAuth?: HttpAuthService;
|
||||
}
|
||||
? {
|
||||
httpAuth: HttpAuthService;
|
||||
}
|
||||
: 'error: at least one of auth and/or httpAuth must be provided',
|
||||
: {}),
|
||||
>(options: TOptions): TAdapters;
|
||||
|
||||
// @public
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/backend-common",
|
||||
"description": "Common functionality library for Backstage backends",
|
||||
"version": "0.21.0",
|
||||
"version": "0.21.3-next.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -13,8 +13,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { mockServices } from '@backstage/backend-test-utils';
|
||||
import { createLegacyAuthAdapters } from './createLegacyAuthAdapters';
|
||||
import { Request } from 'express';
|
||||
|
||||
describe('createLegacyAuthAdapters', () => {
|
||||
it('should pass through auth if only auth is provided', () => {
|
||||
@@ -56,7 +58,22 @@ describe('createLegacyAuthAdapters', () => {
|
||||
expect(ret.httpAuth).toBe(httpAuth);
|
||||
});
|
||||
|
||||
it('should adapt both auth and httpAuth if neither are provided', () => {
|
||||
it('should pass through userInfo if it is provided', () => {
|
||||
const auth = {};
|
||||
const userInfo = {};
|
||||
const ret = createLegacyAuthAdapters({
|
||||
auth: auth as any,
|
||||
userInfo: userInfo as any,
|
||||
tokenManager: mockServices.tokenManager(),
|
||||
discovery: {} as any,
|
||||
identity: mockServices.identity(),
|
||||
});
|
||||
|
||||
expect(ret.auth).toBe(auth);
|
||||
expect(ret.userInfo).toBe(userInfo);
|
||||
});
|
||||
|
||||
it('should adapt all services if none are provided', () => {
|
||||
const ret = createLegacyAuthAdapters({
|
||||
auth: undefined,
|
||||
httpAuth: undefined,
|
||||
@@ -68,6 +85,57 @@ describe('createLegacyAuthAdapters', () => {
|
||||
expect(ret).toEqual({
|
||||
auth: expect.any(Object),
|
||||
httpAuth: expect.any(Object),
|
||||
userInfo: expect.any(Object),
|
||||
});
|
||||
});
|
||||
|
||||
it('should forward tokens if no token manager is provided', async () => {
|
||||
const { auth, httpAuth } = createLegacyAuthAdapters({
|
||||
auth: undefined,
|
||||
httpAuth: undefined,
|
||||
discovery: {} as any,
|
||||
identity: mockServices.identity(),
|
||||
});
|
||||
|
||||
const credentials = await httpAuth.credentials({
|
||||
headers: {
|
||||
authorization: 'Bearer my-token',
|
||||
},
|
||||
} as Request);
|
||||
|
||||
await expect(
|
||||
auth.getPluginRequestToken({
|
||||
onBehalfOf: credentials,
|
||||
targetPluginId: 'test',
|
||||
}),
|
||||
).resolves.toEqual({ token: 'my-token' });
|
||||
});
|
||||
|
||||
it('should issue a new token if a token manager is provided', async () => {
|
||||
const { auth, httpAuth } = createLegacyAuthAdapters({
|
||||
auth: undefined,
|
||||
httpAuth: undefined,
|
||||
tokenManager: {
|
||||
...mockServices.tokenManager(),
|
||||
async getToken() {
|
||||
return { token: 'new-token' };
|
||||
},
|
||||
},
|
||||
discovery: {} as any,
|
||||
identity: mockServices.identity(),
|
||||
});
|
||||
|
||||
const credentials = await httpAuth.credentials({
|
||||
headers: {
|
||||
authorization: 'Bearer mock-token',
|
||||
},
|
||||
} as Request);
|
||||
|
||||
await expect(
|
||||
auth.getPluginRequestToken({
|
||||
onBehalfOf: credentials,
|
||||
targetPluginId: 'test',
|
||||
}),
|
||||
).resolves.toEqual({ token: 'new-token' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,14 +17,17 @@
|
||||
import {
|
||||
AuthService,
|
||||
BackstageCredentials,
|
||||
BackstageNonePrincipal,
|
||||
BackstagePrincipalTypes,
|
||||
BackstageServicePrincipal,
|
||||
BackstageUserInfo,
|
||||
BackstageUserPrincipal,
|
||||
HttpAuthService,
|
||||
IdentityService,
|
||||
TokenManagerService,
|
||||
UserInfoService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { ServerTokenManager, TokenManager } from '../tokens';
|
||||
import { TokenManager } from '../tokens';
|
||||
import { AuthenticationError, NotAllowedError } from '@backstage/errors';
|
||||
import type { Request, Response } from 'express';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
@@ -45,7 +48,7 @@ import { PluginEndpointDiscovery } from '../discovery';
|
||||
class AuthCompat implements AuthService {
|
||||
constructor(
|
||||
private readonly identity: IdentityService,
|
||||
private readonly tokenManager: TokenManagerService,
|
||||
private readonly tokenManager?: TokenManagerService,
|
||||
) {}
|
||||
|
||||
isPrincipal<TType extends keyof BackstagePrincipalTypes>(
|
||||
@@ -63,6 +66,12 @@ class AuthCompat implements AuthService {
|
||||
return true;
|
||||
}
|
||||
|
||||
async getNoneCredentials(): Promise<
|
||||
BackstageCredentials<BackstageNonePrincipal>
|
||||
> {
|
||||
return createCredentialsWithNonePrincipal();
|
||||
}
|
||||
|
||||
async getOwnServiceCredentials(): Promise<
|
||||
BackstageCredentials<BackstageServicePrincipal>
|
||||
> {
|
||||
@@ -70,9 +79,12 @@ class AuthCompat implements AuthService {
|
||||
}
|
||||
|
||||
async authenticate(token: string): Promise<BackstageCredentials> {
|
||||
const { aud } = decodeJwt(token);
|
||||
// Defensively check whether it seems token-like first, just to support
|
||||
// custom TokenManager implementations that don't emit JWTs specifically.
|
||||
const payload =
|
||||
token.split('.').length === 3 ? decodeJwt(token) : undefined;
|
||||
|
||||
if (aud === 'backstage') {
|
||||
if (payload?.aud === 'backstage') {
|
||||
// User Backstage token
|
||||
const identity = await this.identity.getIdentity({
|
||||
request: {
|
||||
@@ -87,12 +99,16 @@ class AuthCompat implements AuthService {
|
||||
return createCredentialsWithUserPrincipal(
|
||||
identity.identity.userEntityRef,
|
||||
token,
|
||||
this.#getJwtExpiration(token),
|
||||
);
|
||||
}
|
||||
|
||||
await this.tokenManager.authenticate(token);
|
||||
await this.tokenManager?.authenticate(token);
|
||||
|
||||
return createCredentialsWithServicePrincipal('external:backstage-plugin');
|
||||
return createCredentialsWithServicePrincipal(
|
||||
'external:backstage-plugin',
|
||||
token,
|
||||
);
|
||||
}
|
||||
|
||||
async getPluginRequestToken(options: {
|
||||
@@ -104,8 +120,12 @@ class AuthCompat implements AuthService {
|
||||
|
||||
switch (type) {
|
||||
// TODO: Check whether the principal is ourselves
|
||||
case 'service':
|
||||
return this.tokenManager.getToken();
|
||||
case 'service': {
|
||||
if (this.tokenManager) {
|
||||
return this.tokenManager.getToken();
|
||||
}
|
||||
return { token: internalForward.token ?? '' };
|
||||
}
|
||||
case 'user':
|
||||
if (!internalForward.token) {
|
||||
throw new Error('User credentials is unexpectedly missing token');
|
||||
@@ -121,6 +141,30 @@ class AuthCompat implements AuthService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getLimitedUserToken(
|
||||
credentials: BackstageCredentials<BackstageUserPrincipal>,
|
||||
): Promise<{ token: string; expiresAt: Date }> {
|
||||
const internalCredentials = toInternalBackstageCredentials(credentials);
|
||||
|
||||
const { token } = internalCredentials;
|
||||
|
||||
if (!token) {
|
||||
throw new AuthenticationError(
|
||||
'User credentials is unexpectedly missing token',
|
||||
);
|
||||
}
|
||||
|
||||
return { token, expiresAt: this.#getJwtExpiration(token) };
|
||||
}
|
||||
|
||||
#getJwtExpiration(token: string) {
|
||||
const { exp } = decodeJwt(token);
|
||||
if (!exp) {
|
||||
throw new AuthenticationError('User token is missing expiration');
|
||||
}
|
||||
return new Date(exp * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function getTokenFromRequest(req: Request) {
|
||||
@@ -144,22 +188,22 @@ type RequestWithCredentials = Request & {
|
||||
};
|
||||
|
||||
class HttpAuthCompat implements HttpAuthService {
|
||||
constructor(private readonly auth: AuthService) {}
|
||||
#auth: AuthService;
|
||||
|
||||
constructor(auth: AuthService) {
|
||||
this.#auth = auth;
|
||||
}
|
||||
|
||||
async #extractCredentialsFromRequest(req: Request) {
|
||||
const token = getTokenFromRequest(req);
|
||||
if (!token) {
|
||||
return createCredentialsWithNonePrincipal();
|
||||
return this.#auth.getNoneCredentials();
|
||||
}
|
||||
|
||||
const credentials = toInternalBackstageCredentials(
|
||||
await this.auth.authenticate(token),
|
||||
);
|
||||
|
||||
return credentials;
|
||||
return this.#auth.authenticate(token);
|
||||
}
|
||||
|
||||
async #getCredentials(req: /* */ RequestWithCredentials) {
|
||||
async #getCredentials(req: RequestWithCredentials) {
|
||||
return (req[credentialsSymbol] ??=
|
||||
this.#extractCredentialsFromRequest(req));
|
||||
}
|
||||
@@ -168,39 +212,77 @@ class HttpAuthCompat implements HttpAuthService {
|
||||
req: Request,
|
||||
options?: {
|
||||
allow?: Array<TAllowed>;
|
||||
allowedAuthMethods?: Array<'token' | 'cookie'>;
|
||||
allowLimitedAccess?: boolean;
|
||||
},
|
||||
): Promise<BackstageCredentials<BackstagePrincipalTypes[TAllowed]>> {
|
||||
const credentials = toInternalBackstageCredentials(
|
||||
await this.#getCredentials(req),
|
||||
const credentials = await this.#getCredentials(req);
|
||||
|
||||
const allowed = options?.allow;
|
||||
if (!allowed) {
|
||||
return credentials as any;
|
||||
}
|
||||
|
||||
if (this.#auth.isPrincipal(credentials, 'none')) {
|
||||
if (allowed.includes('none' as TAllowed)) {
|
||||
return credentials as any;
|
||||
}
|
||||
|
||||
throw new AuthenticationError('Missing credentials');
|
||||
} else if (this.#auth.isPrincipal(credentials, 'user')) {
|
||||
if (allowed.includes('user' as TAllowed)) {
|
||||
return credentials as any;
|
||||
}
|
||||
|
||||
throw new NotAllowedError(
|
||||
`This endpoint does not allow 'user' credentials`,
|
||||
);
|
||||
} else if (this.#auth.isPrincipal(credentials, 'service')) {
|
||||
if (allowed.includes('service' as TAllowed)) {
|
||||
return credentials as any;
|
||||
}
|
||||
|
||||
throw new NotAllowedError(
|
||||
`This endpoint does not allow 'service' credentials`,
|
||||
);
|
||||
}
|
||||
|
||||
throw new NotAllowedError(
|
||||
'Unknown principal type, this should never happen',
|
||||
);
|
||||
|
||||
const allowedPrincipalTypes = options?.allow;
|
||||
const allowedAuthMethods: Array<'token' | 'cookie' | 'none'> =
|
||||
options?.allowedAuthMethods ?? ['token'];
|
||||
|
||||
if (
|
||||
credentials.authMethod !== 'none' &&
|
||||
!allowedAuthMethods.includes(credentials.authMethod)
|
||||
) {
|
||||
throw new NotAllowedError(
|
||||
`This endpoint does not allow the '${credentials.authMethod}' auth method`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
allowedPrincipalTypes &&
|
||||
!allowedPrincipalTypes.includes(credentials.principal.type as TAllowed)
|
||||
) {
|
||||
throw new NotAllowedError(
|
||||
`This endpoint does not allow '${credentials.principal.type}' credentials`,
|
||||
);
|
||||
}
|
||||
|
||||
return credentials as any;
|
||||
}
|
||||
|
||||
async issueUserCookie(_res: Response): Promise<void> {}
|
||||
async issueUserCookie(_res: Response): Promise<{ expiresAt: Date }> {
|
||||
return { expiresAt: new Date(Date.now() + 3600_000) };
|
||||
}
|
||||
}
|
||||
|
||||
export class UserInfoCompat implements UserInfoService {
|
||||
async getUserInfo(
|
||||
credentials: BackstageCredentials,
|
||||
): Promise<BackstageUserInfo> {
|
||||
const internalCredentials = toInternalBackstageCredentials(credentials);
|
||||
if (internalCredentials.principal.type !== 'user') {
|
||||
throw new Error('Only user credentials are supported');
|
||||
}
|
||||
if (!internalCredentials.token) {
|
||||
throw new Error('User credentials is unexpectedly missing token');
|
||||
}
|
||||
const { sub: userEntityRef, ent: ownershipEntityRefs = [] } = decodeJwt(
|
||||
internalCredentials.token,
|
||||
);
|
||||
|
||||
if (typeof userEntityRef !== 'string') {
|
||||
throw new Error('User entity ref must be a string');
|
||||
}
|
||||
if (
|
||||
!Array.isArray(ownershipEntityRefs) ||
|
||||
ownershipEntityRefs.some(ref => typeof ref !== 'string')
|
||||
) {
|
||||
throw new Error('Ownership entity refs must be an array of strings');
|
||||
}
|
||||
|
||||
return { userEntityRef, ownershipEntityRefs };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,51 +293,60 @@ export function createLegacyAuthAdapters<
|
||||
TOptions extends {
|
||||
auth?: AuthService;
|
||||
httpAuth?: HttpAuthService;
|
||||
userInfo?: UserInfoService;
|
||||
identity?: IdentityService;
|
||||
tokenManager?: TokenManager;
|
||||
discovery: PluginEndpointDiscovery;
|
||||
},
|
||||
TAdapters = TOptions extends {
|
||||
auth?: AuthService;
|
||||
}
|
||||
? TOptions extends { httpAuth?: HttpAuthService }
|
||||
? { auth: AuthService; httpAuth: HttpAuthService }
|
||||
: { auth: AuthService }
|
||||
: TOptions extends { httpAuth?: HttpAuthService }
|
||||
? { httpAuth: HttpAuthService }
|
||||
: 'error: at least one of auth and/or httpAuth must be provided',
|
||||
TAdapters = (TOptions extends { auth?: AuthService }
|
||||
? { auth: AuthService }
|
||||
: {}) &
|
||||
(TOptions extends { httpAuth?: HttpAuthService }
|
||||
? { httpAuth: HttpAuthService }
|
||||
: {}) &
|
||||
(TOptions extends { userInfo?: UserInfoService }
|
||||
? { userInfo: UserInfoService }
|
||||
: {}),
|
||||
>(options: TOptions): TAdapters {
|
||||
const { auth, httpAuth, discovery } = options;
|
||||
const {
|
||||
auth,
|
||||
httpAuth,
|
||||
userInfo = new UserInfoCompat(),
|
||||
discovery,
|
||||
} = options;
|
||||
|
||||
if (auth && httpAuth) {
|
||||
return {
|
||||
auth,
|
||||
httpAuth,
|
||||
userInfo,
|
||||
} as TAdapters;
|
||||
}
|
||||
|
||||
if (auth) {
|
||||
return {
|
||||
auth,
|
||||
userInfo,
|
||||
} as TAdapters;
|
||||
}
|
||||
|
||||
if (httpAuth) {
|
||||
return {
|
||||
httpAuth,
|
||||
userInfo,
|
||||
} as TAdapters;
|
||||
}
|
||||
|
||||
const identity =
|
||||
options.identity ?? DefaultIdentityClient.create({ discovery });
|
||||
const tokenManager = options.tokenManager ?? ServerTokenManager.noop();
|
||||
|
||||
const authImpl = new AuthCompat(identity, tokenManager);
|
||||
const authImpl = new AuthCompat(identity, options.tokenManager);
|
||||
|
||||
const httpAuthImpl = new HttpAuthCompat(authImpl);
|
||||
|
||||
return {
|
||||
auth: authImpl,
|
||||
httpAuth: httpAuthImpl,
|
||||
userInfo,
|
||||
} as TAdapters;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# @backstage/backend-defaults
|
||||
|
||||
## 0.2.13-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7cbb760: Added support for the new auth services, which are now installed by default. See the [migration guide](https://backstage.io/docs/tutorials/auth-service-migration) for details.
|
||||
- Updated dependencies
|
||||
- @backstage/backend-common@0.21.3-next.0
|
||||
- @backstage/backend-app-api@0.6.0-next.0
|
||||
|
||||
## 0.2.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/backend-defaults",
|
||||
"description": "Backend defaults used by Backstage backend apps",
|
||||
"version": "0.2.10",
|
||||
"version": "0.2.13-next.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
# @backstage/backend-dynamic-feature-service
|
||||
|
||||
## 0.2.3-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5247909: Add `events: EventsService` to `LegacyPluginEnvironment`.
|
||||
- Updated dependencies
|
||||
- @backstage/plugin-events-backend@0.3.0-next.0
|
||||
- @backstage/plugin-events-node@0.3.0-next.0
|
||||
- @backstage/backend-common@0.21.3-next.0
|
||||
- @backstage/plugin-auth-node@0.4.8-next.0
|
||||
- @backstage/errors@1.2.4-next.0
|
||||
- @backstage/plugin-scaffolder-node@0.3.3-next.0
|
||||
- @backstage/backend-plugin-api@0.6.13-next.0
|
||||
- @backstage/backend-app-api@0.6.0-next.0
|
||||
- @backstage/plugin-catalog-backend@1.18.0-next.0
|
||||
- @backstage/plugin-permission-common@0.7.13-next.0
|
||||
- @backstage/plugin-search-common@1.2.11-next.0
|
||||
- @backstage/backend-tasks@0.5.18-next.0
|
||||
- @backstage/plugin-search-backend-node@1.2.17-next.0
|
||||
- @backstage/plugin-permission-node@0.7.24-next.0
|
||||
- @backstage/cli-node@0.2.4-next.0
|
||||
- @backstage/config-loader@1.6.3-next.0
|
||||
- @backstage/config@1.1.2-next.0
|
||||
- @backstage/plugin-app-node@0.1.13-next.0
|
||||
- @backstage/cli-common@0.1.13
|
||||
- @backstage/types@1.1.1
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/backend-dynamic-feature-service",
|
||||
"description": "Backstage dynamic feature service",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.3-next.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,5 +1,53 @@
|
||||
# example-backend-next
|
||||
|
||||
## 0.0.21-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/plugin-linguist-backend@0.5.10-next.0
|
||||
- @backstage/plugin-auth-node@0.4.8-next.0
|
||||
- @backstage/plugin-lighthouse-backend@0.4.5-next.0
|
||||
- @backstage/plugin-auth-backend-module-guest-provider@0.1.0-next.0
|
||||
- @backstage/plugin-playlist-backend@0.3.17-next.0
|
||||
- @backstage/backend-plugin-api@0.6.13-next.0
|
||||
- @backstage/plugin-entity-feedback-backend@0.2.10-next.0
|
||||
- @backstage/plugin-notifications-backend@0.1.0-next.0
|
||||
- @backstage/plugin-catalog-backend@1.18.0-next.0
|
||||
- @backstage/plugin-auth-backend@0.22.0-next.0
|
||||
- @backstage/plugin-jenkins-backend@0.4.0-next.0
|
||||
- @backstage/plugin-azure-devops-backend@0.6.0-next.0
|
||||
- @backstage/plugin-scaffolder-backend-module-github@0.2.3-next.0
|
||||
- @backstage/plugin-scaffolder-backend@1.22.0-next.0
|
||||
- @backstage/plugin-permission-common@0.7.13-next.0
|
||||
- @backstage/plugin-search-backend-module-techdocs@0.1.17-next.0
|
||||
- @backstage/plugin-search-backend-module-catalog@0.1.17-next.0
|
||||
- @backstage/plugin-search-backend-module-explore@0.1.17-next.0
|
||||
- @backstage/backend-defaults@0.2.13-next.0
|
||||
- @backstage/plugin-kubernetes-backend@0.16.0-next.0
|
||||
- @backstage/plugin-adr-backend@0.4.10-next.0
|
||||
- @backstage/plugin-proxy-backend@0.4.11-next.0
|
||||
- @backstage/backend-tasks@0.5.18-next.0
|
||||
- @backstage/plugin-search-backend-node@1.2.17-next.0
|
||||
- @backstage/plugin-signals-backend@0.0.4-next.0
|
||||
- @backstage/plugin-search-backend@1.5.3-next.0
|
||||
- @backstage/plugin-devtools-backend@0.3.0-next.0
|
||||
- @backstage/plugin-permission-node@0.7.24-next.0
|
||||
- @backstage/plugin-catalog-backend-module-backstage-openapi@0.1.6-next.0
|
||||
- @backstage/plugin-badges-backend@0.3.10-next.0
|
||||
- @backstage/plugin-permission-backend@0.5.36-next.0
|
||||
- @backstage/plugin-app-backend@0.3.61-next.0
|
||||
- @backstage/plugin-auth-backend-module-github-provider@0.1.10-next.0
|
||||
- @backstage/plugin-catalog-backend-module-openapi@0.1.30-next.0
|
||||
- @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.10-next.0
|
||||
- @backstage/plugin-permission-backend-module-allow-all-policy@0.1.10-next.0
|
||||
- @backstage/plugin-sonarqube-backend@0.2.15-next.0
|
||||
- @backstage/plugin-techdocs-backend@1.9.6-next.0
|
||||
- @backstage/plugin-nomad-backend@0.1.15-next.0
|
||||
- @backstage/plugin-todo-backend@0.3.11-next.0
|
||||
- @backstage/plugin-catalog-backend-module-unprocessed@0.3.10-next.0
|
||||
- @backstage/catalog-model@1.4.5-next.0
|
||||
|
||||
## 0.0.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-backend-next",
|
||||
"version": "0.0.20",
|
||||
"version": "0.0.21-next.0",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -33,6 +33,7 @@
|
||||
"@backstage/plugin-app-backend": "workspace:^",
|
||||
"@backstage/plugin-auth-backend": "workspace:^",
|
||||
"@backstage/plugin-auth-backend-module-github-provider": "workspace:^",
|
||||
"@backstage/plugin-auth-backend-module-guest-provider": "workspace:^",
|
||||
"@backstage/plugin-auth-node": "workspace:^",
|
||||
"@backstage/plugin-azure-devops-backend": "workspace:^",
|
||||
"@backstage/plugin-badges-backend": "workspace:^",
|
||||
@@ -56,6 +57,7 @@
|
||||
"@backstage/plugin-playlist-backend": "workspace:^",
|
||||
"@backstage/plugin-proxy-backend": "workspace:^",
|
||||
"@backstage/plugin-scaffolder-backend": "workspace:^",
|
||||
"@backstage/plugin-scaffolder-backend-module-github": "workspace:^",
|
||||
"@backstage/plugin-search-backend": "workspace:^",
|
||||
"@backstage/plugin-search-backend-module-catalog": "workspace:^",
|
||||
"@backstage/plugin-search-backend-module-explore": "workspace:^",
|
||||
|
||||
@@ -20,6 +20,7 @@ const backend = createBackend();
|
||||
|
||||
backend.add(import('@backstage/plugin-auth-backend'));
|
||||
backend.add(import('./authModuleGithubProvider'));
|
||||
backend.add(import('@backstage/plugin-auth-backend-module-guest-provider'));
|
||||
|
||||
backend.add(import('@backstage/plugin-adr-backend'));
|
||||
backend.add(import('@backstage/plugin-app-backend/alpha'));
|
||||
@@ -44,6 +45,7 @@ backend.add(
|
||||
backend.add(import('@backstage/plugin-permission-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-proxy-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend-module-github'));
|
||||
backend.add(import('@backstage/plugin-search-backend-module-catalog/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend-module-explore/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha'));
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# @backstage/backend-openapi-utils
|
||||
|
||||
## 0.1.6-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 85ec23e: Updated dependency `json-schema-to-ts` to `^3.0.0`.
|
||||
- Updated dependencies
|
||||
- @backstage/errors@1.2.4-next.0
|
||||
- @backstage/backend-plugin-api@0.6.13-next.0
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/backend-openapi-utils",
|
||||
"description": "OpenAPI typescript support.",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.6-next.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# @backstage/backend-plugin-api
|
||||
|
||||
## 0.6.13-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4a3d434: Added the new [`auth`](https://backstage.io/docs/backend-system/core-services/auth/), [`httpAuth`](https://backstage.io/docs/backend-system/core-services/http-auth), and [`userInfo`](https://backstage.io/docs/backend-system/core-services/user-info) services that were created as part of [BEP-0003](https://github.com/backstage/backstage/tree/master/beps/0003-auth-architecture-evolution) to the `coreServices`.
|
||||
|
||||
At the same time, the [`httpRouter`](https://backstage.io/docs/backend-system/core-services/http-router) service gained a new `addAuthPolicy` method that lets your plugin declare exemptions to the default auth policy - for example if you want to allow unauthenticated or cookie-based access to some subset of your feature routes.
|
||||
|
||||
If you have migrated to the new backend system, please see the [Auth Service Migration tutorial](https://backstage.io/docs/tutorials/auth-service-migration) for more information on how to move toward using these services.
|
||||
|
||||
- 0502d82: Updated the `PermissionsService` methods to accept `BackstageCredentials` through options.
|
||||
- Updated dependencies
|
||||
- @backstage/plugin-auth-node@0.4.8-next.0
|
||||
- @backstage/plugin-permission-common@0.7.13-next.0
|
||||
- @backstage/backend-tasks@0.5.18-next.0
|
||||
- @backstage/config@1.1.2-next.0
|
||||
- @backstage/types@1.1.1
|
||||
|
||||
## 0.6.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -24,7 +24,21 @@ import { Response as Response_2 } from 'express';
|
||||
// @public (undocumented)
|
||||
export interface AuthService {
|
||||
// (undocumented)
|
||||
authenticate(token: string): Promise<BackstageCredentials>;
|
||||
authenticate(
|
||||
token: string,
|
||||
options?: {
|
||||
allowLimitedAccess?: boolean;
|
||||
},
|
||||
): Promise<BackstageCredentials>;
|
||||
// (undocumented)
|
||||
getLimitedUserToken(
|
||||
credentials: BackstageCredentials<BackstageUserPrincipal>,
|
||||
): Promise<{
|
||||
token: string;
|
||||
expiresAt: Date;
|
||||
}>;
|
||||
// (undocumented)
|
||||
getNoneCredentials(): Promise<BackstageCredentials<BackstageNonePrincipal>>;
|
||||
// (undocumented)
|
||||
getOwnServiceCredentials(): Promise<
|
||||
BackstageCredentials<BackstageServicePrincipal>
|
||||
@@ -107,6 +121,7 @@ export interface BackendPluginRegistrationPoints {
|
||||
// @public (undocumented)
|
||||
export type BackstageCredentials<TPrincipal = unknown> = {
|
||||
$$type: '@backstage/BackstageCredentials';
|
||||
expiresAt?: Date;
|
||||
principal: TPrincipal;
|
||||
};
|
||||
|
||||
@@ -299,11 +314,18 @@ export interface HttpAuthService {
|
||||
req: Request_2<any, any, any, any, any>,
|
||||
options?: {
|
||||
allow?: Array<TAllowed>;
|
||||
allowedAuthMethods?: Array<'token' | 'cookie'>;
|
||||
allowLimitedAccess?: boolean;
|
||||
},
|
||||
): Promise<BackstageCredentials<BackstagePrincipalTypes[TAllowed]>>;
|
||||
// (undocumented)
|
||||
issueUserCookie(res: Response_2): Promise<void>;
|
||||
issueUserCookie(
|
||||
res: Response_2,
|
||||
options?: {
|
||||
credentials?: BackstageCredentials<BackstageUserPrincipal>;
|
||||
},
|
||||
): Promise<{
|
||||
expiresAt: Date;
|
||||
}>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/backend-plugin-api",
|
||||
"description": "Core API used by Backstage backend plugins",
|
||||
"version": "0.6.10",
|
||||
"version": "0.6.13-next.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -46,6 +46,8 @@ export type BackstageServicePrincipal = {
|
||||
export type BackstageCredentials<TPrincipal = unknown> = {
|
||||
$$type: '@backstage/BackstageCredentials';
|
||||
|
||||
expiresAt?: Date;
|
||||
|
||||
principal: TPrincipal;
|
||||
};
|
||||
|
||||
@@ -63,13 +65,20 @@ export type BackstagePrincipalTypes = {
|
||||
* @public
|
||||
*/
|
||||
export interface AuthService {
|
||||
authenticate(token: string): Promise<BackstageCredentials>;
|
||||
authenticate(
|
||||
token: string,
|
||||
options?: {
|
||||
allowLimitedAccess?: boolean;
|
||||
},
|
||||
): Promise<BackstageCredentials>;
|
||||
|
||||
isPrincipal<TType extends keyof BackstagePrincipalTypes>(
|
||||
credentials: BackstageCredentials,
|
||||
type: TType,
|
||||
): credentials is BackstageCredentials<BackstagePrincipalTypes[TType]>;
|
||||
|
||||
getNoneCredentials(): Promise<BackstageCredentials<BackstageNonePrincipal>>;
|
||||
|
||||
getOwnServiceCredentials(): Promise<
|
||||
BackstageCredentials<BackstageServicePrincipal>
|
||||
>;
|
||||
@@ -78,4 +87,8 @@ export interface AuthService {
|
||||
onBehalfOf: BackstageCredentials;
|
||||
targetPluginId: string;
|
||||
}): Promise<{ token: string }>;
|
||||
|
||||
getLimitedUserToken(
|
||||
credentials: BackstageCredentials<BackstageUserPrincipal>,
|
||||
): Promise<{ token: string; expiresAt: Date }>;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
*/
|
||||
|
||||
import { Request, Response } from 'express';
|
||||
import { BackstageCredentials, BackstagePrincipalTypes } from './AuthService';
|
||||
import {
|
||||
BackstageCredentials,
|
||||
BackstagePrincipalTypes,
|
||||
BackstageUserPrincipal,
|
||||
} from './AuthService';
|
||||
|
||||
/** @public */
|
||||
export interface HttpAuthService {
|
||||
@@ -23,9 +27,14 @@ export interface HttpAuthService {
|
||||
req: Request<any, any, any, any, any>,
|
||||
options?: {
|
||||
allow?: Array<TAllowed>;
|
||||
allowedAuthMethods?: Array<'token' | 'cookie'>;
|
||||
allowLimitedAccess?: boolean;
|
||||
},
|
||||
): Promise<BackstageCredentials<BackstagePrincipalTypes[TAllowed]>>;
|
||||
|
||||
issueUserCookie(res: Response): Promise<void>;
|
||||
issueUserCookie(
|
||||
res: Response,
|
||||
options?: {
|
||||
credentials?: BackstageCredentials<BackstageUserPrincipal>;
|
||||
},
|
||||
): Promise<{ expiresAt: Date }>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @backstage/backend-tasks
|
||||
|
||||
## 0.5.18-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 0fb419b: Updated dependency `uuid` to `^9.0.0`.
|
||||
Updated dependency `@types/uuid` to `^9.0.0`.
|
||||
- Updated dependencies
|
||||
- @backstage/backend-common@0.21.3-next.0
|
||||
- @backstage/errors@1.2.4-next.0
|
||||
- @backstage/config@1.1.2-next.0
|
||||
- @backstage/types@1.1.1
|
||||
|
||||
## 0.5.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/backend-tasks",
|
||||
"description": "Common distributed task management library for Backstage backends",
|
||||
"version": "0.5.15",
|
||||
"version": "0.5.18-next.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# @backstage/backend-test-utils
|
||||
|
||||
## 0.3.3-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4a3d434: Added support for the new [`auth`](https://backstage.io/docs/backend-system/core-services/auth/) and [`httpAuth`](https://backstage.io/docs/backend-system/core-services/http-auth) services that were created as part of [BEP-0003](https://github.com/backstage/backstage/tree/master/beps/0003-auth-architecture-evolution). These services will be present by default in test apps, and you can access mocked versions of their features under `mockServices.auth` and `mockServices.httpAuth` if you want to inspect or replace their behaviors.
|
||||
|
||||
There is also a new `mockCredentials` that you can use for acquiring mocks of the various types of credentials that are used in the new system.
|
||||
|
||||
- 9802004: Added `mockServices.userInfo`, which now also automatically is made available in test backends.
|
||||
- fd61d39: Updated dependency `testcontainers` to `^10.0.0`.
|
||||
- ff40ada: Updated dependency `mysql2` to `^3.0.0`.
|
||||
- 0fb419b: Updated dependency `uuid` to `^9.0.0`.
|
||||
Updated dependency `@types/uuid` to `^9.0.0`.
|
||||
- Updated dependencies
|
||||
- @backstage/backend-common@0.21.3-next.0
|
||||
- @backstage/plugin-auth-node@0.4.8-next.0
|
||||
- @backstage/errors@1.2.4-next.0
|
||||
- @backstage/backend-plugin-api@0.6.13-next.0
|
||||
- @backstage/backend-app-api@0.6.0-next.0
|
||||
- @backstage/config@1.1.2-next.0
|
||||
- @backstage/types@1.1.1
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -12,6 +12,7 @@ import { BackendFeature } from '@backstage/backend-plugin-api';
|
||||
import { BackstageCredentials } from '@backstage/backend-plugin-api';
|
||||
import { BackstageNonePrincipal } from '@backstage/backend-plugin-api';
|
||||
import { BackstageServicePrincipal } from '@backstage/backend-plugin-api';
|
||||
import { BackstageUserInfo } from '@backstage/backend-plugin-api';
|
||||
import { BackstageUserPrincipal } from '@backstage/backend-plugin-api';
|
||||
import { CacheService } from '@backstage/backend-plugin-api';
|
||||
import { DatabaseService } from '@backstage/backend-plugin-api';
|
||||
@@ -37,6 +38,7 @@ import { ServiceFactory } from '@backstage/backend-plugin-api';
|
||||
import { ServiceRef } from '@backstage/backend-plugin-api';
|
||||
import { TokenManagerService } from '@backstage/backend-plugin-api';
|
||||
import { UrlReaderService } from '@backstage/backend-plugin-api';
|
||||
import { UserInfoService } from '@backstage/backend-plugin-api';
|
||||
|
||||
// @public
|
||||
export function createMockDirectory(
|
||||
@@ -48,6 +50,17 @@ export function isDockerDisabledForTests(): boolean;
|
||||
|
||||
// @public (undocumented)
|
||||
export namespace mockCredentials {
|
||||
export function limitedUser(
|
||||
userEntityRef?: string,
|
||||
): BackstageCredentials<BackstageUserPrincipal>;
|
||||
export namespace limitedUser {
|
||||
export function cookie(userEntityRef?: string): string;
|
||||
// (undocumented)
|
||||
export function invalidCookie(): string;
|
||||
// (undocumented)
|
||||
export function invalidToken(): string;
|
||||
export function token(userEntityRef?: string): string;
|
||||
}
|
||||
export function none(): BackstageCredentials<BackstageNonePrincipal>;
|
||||
export namespace none {
|
||||
export function header(): string;
|
||||
@@ -316,6 +329,17 @@ export namespace mockServices {
|
||||
partialImpl?: Partial<UrlReaderService> | undefined,
|
||||
) => ServiceMock<UrlReaderService>;
|
||||
}
|
||||
export function userInfo(
|
||||
customInfo?: Partial<BackstageUserInfo>,
|
||||
): UserInfoService;
|
||||
// (undocumented)
|
||||
export namespace userInfo {
|
||||
const factory: () => ServiceFactory<UserInfoService, 'plugin'>;
|
||||
const // (undocumented)
|
||||
mock: (
|
||||
partialImpl?: Partial<UserInfoService> | undefined,
|
||||
) => ServiceMock<UserInfoService>;
|
||||
}
|
||||
}
|
||||
|
||||
// @public
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
{
|
||||
"name": "@backstage/backend-test-utils",
|
||||
"version": "0.3.3-next.0",
|
||||
"description": "Test helpers library for Backstage backends",
|
||||
"version": "0.3.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"backstage": {
|
||||
"role": "node-library"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage",
|
||||
"test"
|
||||
],
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "packages/backend-test-utils"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"package.json": [
|
||||
@@ -18,28 +32,17 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"backstage": {
|
||||
"role": "node-library"
|
||||
},
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "packages/backend-test-utils"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage",
|
||||
"test"
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"clean": "backstage-cli package clean",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack",
|
||||
"clean": "backstage-cli package clean",
|
||||
"start": "backstage-cli package start"
|
||||
"start": "backstage-cli package start",
|
||||
"test": "backstage-cli package test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-app-api": "workspace:^",
|
||||
@@ -50,6 +53,7 @@
|
||||
"@backstage/plugin-auth-node": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"better-sqlite3": "^9.0.0",
|
||||
"cookie": "^0.6.0",
|
||||
"express": "^4.17.1",
|
||||
"fs-extra": "^11.0.0",
|
||||
"knex": "^3.0.0",
|
||||
@@ -60,15 +64,12 @@
|
||||
"textextensions": "^5.16.0",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/jest": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"supertest": "^6.1.3"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
"peerDependencies": {
|
||||
"@types/jest": "*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,12 @@ describe('MockAuthService', () => {
|
||||
auth.authenticate(mockCredentials.user.token()),
|
||||
).resolves.toEqual(mockCredentials.user());
|
||||
|
||||
await expect(
|
||||
auth.authenticate(mockCredentials.user.token(), {
|
||||
allowLimitedAccess: true,
|
||||
}),
|
||||
).resolves.toEqual(mockCredentials.user());
|
||||
|
||||
await expect(
|
||||
auth.authenticate(mockCredentials.user.token()),
|
||||
).resolves.toEqual(mockCredentials.user(DEFAULT_MOCK_USER_ENTITY_REF));
|
||||
@@ -66,6 +72,44 @@ describe('MockAuthService', () => {
|
||||
).rejects.toThrow('User token is invalid');
|
||||
});
|
||||
|
||||
it('should authenticate mock limited user tokens', async () => {
|
||||
await expect(
|
||||
auth.authenticate(mockCredentials.limitedUser.token()),
|
||||
).rejects.toThrow('Limited user token is not allowed');
|
||||
await expect(
|
||||
auth.authenticate(mockCredentials.limitedUser.token(), {}),
|
||||
).rejects.toThrow('Limited user token is not allowed');
|
||||
await expect(
|
||||
auth.authenticate(mockCredentials.limitedUser.token(), {
|
||||
allowLimitedAccess: false,
|
||||
}),
|
||||
).rejects.toThrow('Limited user token is not allowed');
|
||||
await expect(
|
||||
auth.authenticate(mockCredentials.limitedUser.token(), {
|
||||
allowLimitedAccess: true,
|
||||
}),
|
||||
).resolves.toEqual(mockCredentials.user());
|
||||
|
||||
await expect(
|
||||
auth.authenticate(mockCredentials.limitedUser.token(), {
|
||||
allowLimitedAccess: true,
|
||||
}),
|
||||
).resolves.toEqual(mockCredentials.user(DEFAULT_MOCK_USER_ENTITY_REF));
|
||||
|
||||
await expect(
|
||||
auth.authenticate(
|
||||
mockCredentials.limitedUser.token('user:default/other'),
|
||||
{
|
||||
allowLimitedAccess: true,
|
||||
},
|
||||
),
|
||||
).resolves.toEqual(mockCredentials.user('user:default/other'));
|
||||
|
||||
await expect(
|
||||
auth.authenticate(mockCredentials.limitedUser.invalidToken()),
|
||||
).rejects.toThrow('Limited user token is invalid');
|
||||
});
|
||||
|
||||
it('should authenticate mock service tokens', async () => {
|
||||
await expect(
|
||||
auth.authenticate(mockCredentials.service.token()),
|
||||
@@ -113,6 +157,12 @@ describe('MockAuthService', () => {
|
||||
).rejects.toThrow('Service token is invalid');
|
||||
});
|
||||
|
||||
it('should return none credentials', async () => {
|
||||
await expect(auth.getNoneCredentials()).resolves.toEqual(
|
||||
mockCredentials.none(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return own service credentials', async () => {
|
||||
await expect(auth.getOwnServiceCredentials()).resolves.toEqual(
|
||||
mockCredentials.service('plugin:test'),
|
||||
@@ -211,4 +261,32 @@ describe('MockAuthService', () => {
|
||||
`Refused to issue service token for credential type 'none'`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should issue limited user tokens', async () => {
|
||||
await expect(
|
||||
auth.getLimitedUserToken(mockCredentials.user()),
|
||||
).resolves.toEqual({
|
||||
token: mockCredentials.limitedUser.token(),
|
||||
expiresAt: expect.any(Date),
|
||||
});
|
||||
|
||||
await expect(
|
||||
auth.getLimitedUserToken(mockCredentials.user('user:default/other')),
|
||||
).resolves.toEqual({
|
||||
token: mockCredentials.limitedUser.token('user:default/other'),
|
||||
expiresAt: expect.any(Date),
|
||||
});
|
||||
|
||||
await expect(
|
||||
auth.getLimitedUserToken(mockCredentials.none() as any),
|
||||
).rejects.toThrow(
|
||||
"Refused to issue limited user token for credential type 'none'",
|
||||
);
|
||||
|
||||
await expect(
|
||||
auth.getLimitedUserToken(mockCredentials.service() as any),
|
||||
).rejects.toThrow(
|
||||
"Refused to issue limited user token for credential type 'service'",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,9 +27,11 @@ import {
|
||||
mockCredentials,
|
||||
MOCK_USER_TOKEN,
|
||||
MOCK_USER_TOKEN_PREFIX,
|
||||
MOCK_INVALID_USER_TOKEN,
|
||||
MOCK_USER_LIMITED_TOKEN_PREFIX,
|
||||
MOCK_INVALID_USER_LIMITED_TOKEN,
|
||||
MOCK_SERVICE_TOKEN,
|
||||
MOCK_SERVICE_TOKEN_PREFIX,
|
||||
MOCK_INVALID_USER_TOKEN,
|
||||
MOCK_INVALID_SERVICE_TOKEN,
|
||||
UserTokenPayload,
|
||||
ServiceTokenPayload,
|
||||
@@ -48,7 +50,10 @@ export class MockAuthService implements AuthService {
|
||||
this.disableDefaultAuthPolicy = options.disableDefaultAuthPolicy;
|
||||
}
|
||||
|
||||
async authenticate(token: string): Promise<BackstageCredentials> {
|
||||
async authenticate(
|
||||
token: string,
|
||||
options?: { allowLimitedAccess?: boolean },
|
||||
): Promise<BackstageCredentials> {
|
||||
switch (token) {
|
||||
case MOCK_USER_TOKEN:
|
||||
return mockCredentials.user();
|
||||
@@ -56,6 +61,8 @@ export class MockAuthService implements AuthService {
|
||||
return mockCredentials.service();
|
||||
case MOCK_INVALID_USER_TOKEN:
|
||||
throw new AuthenticationError('User token is invalid');
|
||||
case MOCK_INVALID_USER_LIMITED_TOKEN:
|
||||
throw new AuthenticationError('Limited user token is invalid');
|
||||
case MOCK_INVALID_SERVICE_TOKEN:
|
||||
throw new AuthenticationError('Service token is invalid');
|
||||
case '':
|
||||
@@ -72,6 +79,18 @@ export class MockAuthService implements AuthService {
|
||||
return mockCredentials.user(userEntityRef);
|
||||
}
|
||||
|
||||
if (token.startsWith(MOCK_USER_LIMITED_TOKEN_PREFIX)) {
|
||||
if (!options?.allowLimitedAccess) {
|
||||
throw new AuthenticationError('Limited user token is not allowed');
|
||||
}
|
||||
|
||||
const { sub: userEntityRef }: UserTokenPayload = JSON.parse(
|
||||
token.slice(MOCK_USER_LIMITED_TOKEN_PREFIX.length),
|
||||
);
|
||||
|
||||
return mockCredentials.user(userEntityRef);
|
||||
}
|
||||
|
||||
if (token.startsWith(MOCK_SERVICE_TOKEN_PREFIX)) {
|
||||
const { sub, target, obo }: ServiceTokenPayload = JSON.parse(
|
||||
token.slice(MOCK_SERVICE_TOKEN_PREFIX.length),
|
||||
@@ -92,6 +111,10 @@ export class MockAuthService implements AuthService {
|
||||
throw new AuthenticationError(`Unknown mock token '${token}'`);
|
||||
}
|
||||
|
||||
async getNoneCredentials() {
|
||||
return mockCredentials.none();
|
||||
}
|
||||
|
||||
async getOwnServiceCredentials(): Promise<
|
||||
BackstageCredentials<BackstageServicePrincipal>
|
||||
> {
|
||||
@@ -144,4 +167,21 @@ export class MockAuthService implements AuthService {
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async getLimitedUserToken(
|
||||
credentials: BackstageCredentials<BackstageUserPrincipal>,
|
||||
): Promise<{ token: string; expiresAt: Date }> {
|
||||
if (credentials.principal.type !== 'user') {
|
||||
throw new AuthenticationError(
|
||||
`Refused to issue limited user token for credential type '${credentials.principal.type}'`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
token: mockCredentials.limitedUser.token(
|
||||
credentials.principal.userEntityRef,
|
||||
),
|
||||
expiresAt: new Date(Date.now() + 3600_000),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,11 @@ import { AuthenticationError } from '@backstage/errors';
|
||||
describe('MockHttpAuthService', () => {
|
||||
const httpAuth = new MockHttpAuthService('test', mockCredentials.none());
|
||||
|
||||
function makeAuthReq(header?: string) {
|
||||
return { headers: { authorization: header } } as Request;
|
||||
function makeAuthReq(authorization?: string) {
|
||||
return { headers: { authorization } } as Request;
|
||||
}
|
||||
function makeCookieAuthReq(cookie?: string) {
|
||||
return { headers: { cookie } } as Request;
|
||||
}
|
||||
|
||||
it('should authenticate unauthenticated requests', async () => {
|
||||
@@ -68,6 +71,59 @@ describe('MockHttpAuthService', () => {
|
||||
).resolves.toEqual(mockCredentials.user('user:default/other'));
|
||||
});
|
||||
|
||||
it('should authenticate limited user requests', async () => {
|
||||
await expect(
|
||||
httpAuth.credentials(
|
||||
makeCookieAuthReq(mockCredentials.limitedUser.cookie()),
|
||||
),
|
||||
).resolves.toEqual(mockCredentials.none());
|
||||
|
||||
await expect(
|
||||
httpAuth.credentials(
|
||||
makeCookieAuthReq(mockCredentials.limitedUser.cookie()),
|
||||
{ allowLimitedAccess: true },
|
||||
),
|
||||
).resolves.toEqual(mockCredentials.user());
|
||||
|
||||
await expect(
|
||||
httpAuth.credentials(makeAuthReq(mockCredentials.user.header()), {
|
||||
allowLimitedAccess: true,
|
||||
}),
|
||||
).resolves.toEqual(mockCredentials.user());
|
||||
|
||||
await expect(
|
||||
httpAuth.credentials(
|
||||
makeCookieAuthReq(mockCredentials.limitedUser.cookie()),
|
||||
{
|
||||
allow: ['user'],
|
||||
},
|
||||
),
|
||||
).rejects.toThrow('Missing credentials');
|
||||
|
||||
await expect(
|
||||
httpAuth.credentials(
|
||||
makeCookieAuthReq(mockCredentials.limitedUser.cookie()),
|
||||
{
|
||||
allow: ['none', 'service'],
|
||||
allowLimitedAccess: true,
|
||||
},
|
||||
),
|
||||
).rejects.toThrow("This endpoint does not allow 'user' credentials");
|
||||
|
||||
await expect(
|
||||
httpAuth.credentials(
|
||||
makeAuthReq(`Bearer ${mockCredentials.limitedUser.token()}`),
|
||||
{ allowLimitedAccess: true },
|
||||
),
|
||||
).resolves.toEqual(mockCredentials.user());
|
||||
|
||||
await expect(
|
||||
httpAuth.credentials(
|
||||
makeAuthReq(`Bearer ${mockCredentials.limitedUser.token()}`),
|
||||
),
|
||||
).rejects.toThrow('Limited user token is not allowed');
|
||||
});
|
||||
|
||||
it('should authenticate service requests', async () => {
|
||||
await expect(
|
||||
httpAuth.credentials(makeAuthReq(mockCredentials.service.header())),
|
||||
@@ -161,9 +217,42 @@ describe('MockHttpAuthService', () => {
|
||||
).rejects.toThrow('Service token is invalid');
|
||||
});
|
||||
|
||||
it('does not implement .issueUserCookie', async () => {
|
||||
await expect(httpAuth.issueUserCookie({} as any)).rejects.toThrow(
|
||||
'Not implemented',
|
||||
it('should issue user cookie from request credentials', async () => {
|
||||
const setHeader = jest.fn();
|
||||
|
||||
await expect(
|
||||
httpAuth.issueUserCookie({
|
||||
req: makeAuthReq(mockCredentials.user.header()),
|
||||
setHeader,
|
||||
} as any),
|
||||
).resolves.toEqual({
|
||||
expiresAt: expect.any(Date),
|
||||
});
|
||||
|
||||
expect(setHeader).toHaveBeenCalledWith(
|
||||
'Set-Cookie',
|
||||
mockCredentials.limitedUser.cookie(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should issue user cookie from explicit credentials', async () => {
|
||||
const setHeader = jest.fn();
|
||||
|
||||
await expect(
|
||||
httpAuth.issueUserCookie(
|
||||
{
|
||||
req: makeAuthReq(mockCredentials.user.header()),
|
||||
setHeader,
|
||||
} as any,
|
||||
{ credentials: mockCredentials.user('user:default/other') },
|
||||
),
|
||||
).resolves.toEqual({
|
||||
expiresAt: expect.any(Date),
|
||||
});
|
||||
|
||||
expect(setHeader).toHaveBeenCalledWith(
|
||||
'Set-Cookie',
|
||||
mockCredentials.limitedUser.cookie('user:default/other'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,16 +18,18 @@ import {
|
||||
AuthService,
|
||||
BackstageCredentials,
|
||||
BackstagePrincipalTypes,
|
||||
BackstageUserPrincipal,
|
||||
HttpAuthService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { Request, Response } from 'express';
|
||||
import { parse as parseCookie } from 'cookie';
|
||||
import { MockAuthService } from './MockAuthService';
|
||||
import { AuthenticationError, NotAllowedError } from '@backstage/errors';
|
||||
import {
|
||||
AuthenticationError,
|
||||
NotAllowedError,
|
||||
NotImplementedError,
|
||||
} from '@backstage/errors';
|
||||
import { mockCredentials } from './mockCredentials';
|
||||
MOCK_NONE_TOKEN,
|
||||
MOCK_AUTH_COOKIE,
|
||||
mockCredentials,
|
||||
} from './mockCredentials';
|
||||
|
||||
// TODO: support mock cookie auth?
|
||||
export class MockHttpAuthService implements HttpAuthService {
|
||||
@@ -42,33 +44,52 @@ export class MockHttpAuthService implements HttpAuthService {
|
||||
this.#defaultCredentials = defaultCredentials;
|
||||
}
|
||||
|
||||
async #getCredentials(req: Request) {
|
||||
async #getCredentials(req: Request, allowLimitedAccess: boolean) {
|
||||
const header = req.headers.authorization;
|
||||
|
||||
if (header === mockCredentials.none.header()) {
|
||||
return mockCredentials.none();
|
||||
}
|
||||
|
||||
const token =
|
||||
typeof header === 'string'
|
||||
? header.match(/^Bearer[ ]+(\S+)$/i)?.[1]
|
||||
: undefined;
|
||||
|
||||
if (!token) {
|
||||
return this.#defaultCredentials;
|
||||
if (token) {
|
||||
if (token === MOCK_NONE_TOKEN) {
|
||||
return this.#auth.getNoneCredentials();
|
||||
}
|
||||
|
||||
return await this.#auth.authenticate(token, {
|
||||
allowLimitedAccess,
|
||||
});
|
||||
}
|
||||
|
||||
return await this.#auth.authenticate(token);
|
||||
if (allowLimitedAccess) {
|
||||
const cookieHeader = req.headers.cookie;
|
||||
|
||||
if (cookieHeader) {
|
||||
const cookies = parseCookie(cookieHeader);
|
||||
const cookie = cookies[MOCK_AUTH_COOKIE];
|
||||
|
||||
if (cookie) {
|
||||
return await this.#auth.authenticate(cookie, {
|
||||
allowLimitedAccess: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.#defaultCredentials;
|
||||
}
|
||||
|
||||
async credentials<TAllowed extends keyof BackstagePrincipalTypes = 'unknown'>(
|
||||
req: Request,
|
||||
options?: {
|
||||
allow?: Array<TAllowed>;
|
||||
allowedAuthMethods?: Array<'token' | 'cookie'>;
|
||||
allowLimitedAccess?: boolean;
|
||||
},
|
||||
): Promise<BackstageCredentials<BackstagePrincipalTypes[TAllowed]>> {
|
||||
const credentials = await this.#getCredentials(req);
|
||||
const credentials = await this.#getCredentials(
|
||||
req,
|
||||
options?.allowLimitedAccess ?? false,
|
||||
);
|
||||
|
||||
const allowedPrincipalTypes = options?.allow;
|
||||
if (!allowedPrincipalTypes) {
|
||||
@@ -80,7 +101,7 @@ export class MockHttpAuthService implements HttpAuthService {
|
||||
return credentials as any;
|
||||
}
|
||||
|
||||
throw new AuthenticationError();
|
||||
throw new AuthenticationError('Missing credentials');
|
||||
} else if (this.#auth.isPrincipal(credentials, 'user')) {
|
||||
if (allowedPrincipalTypes.includes('user' as TAllowed)) {
|
||||
return credentials as any;
|
||||
@@ -104,7 +125,19 @@ export class MockHttpAuthService implements HttpAuthService {
|
||||
);
|
||||
}
|
||||
|
||||
async issueUserCookie(_res: Response): Promise<void> {
|
||||
throw new NotImplementedError('Not implemented');
|
||||
async issueUserCookie(
|
||||
res: Response,
|
||||
options?: { credentials?: BackstageCredentials<BackstageUserPrincipal> },
|
||||
): Promise<{ expiresAt: Date }> {
|
||||
const credentials =
|
||||
options?.credentials ??
|
||||
(await this.credentials(res.req, { allow: ['user'] }));
|
||||
|
||||
res.setHeader(
|
||||
'Set-Cookie',
|
||||
mockCredentials.limitedUser.cookie(credentials.principal.userEntityRef),
|
||||
);
|
||||
|
||||
return { expiresAt: new Date(Date.now() + 3600_000) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { MockUserInfoService } from './MockUserInfoService';
|
||||
import { mockCredentials } from './mockCredentials';
|
||||
|
||||
describe('MockUserInfoService', () => {
|
||||
it('works without constructor parameters', async () => {
|
||||
const service = new MockUserInfoService();
|
||||
const user = mockCredentials.user();
|
||||
await expect(service.getUserInfo(user)).resolves.toEqual({
|
||||
userEntityRef: user.principal.userEntityRef,
|
||||
ownershipEntityRefs: [user.principal.userEntityRef],
|
||||
});
|
||||
});
|
||||
|
||||
it('works with custom constructor parameters', async () => {
|
||||
const service = new MockUserInfoService({
|
||||
userEntityRef: 'user:default/not-the-mock-1',
|
||||
ownershipEntityRefs: ['user:default/not-the-mock-2'],
|
||||
});
|
||||
const user = mockCredentials.user();
|
||||
await expect(service.getUserInfo(user)).resolves.toEqual({
|
||||
userEntityRef: 'user:default/not-the-mock-1',
|
||||
ownershipEntityRefs: ['user:default/not-the-mock-2'],
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects non-users', async () => {
|
||||
const service = new MockUserInfoService();
|
||||
await expect(
|
||||
service.getUserInfo(mockCredentials.none()),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"User info not available for principal type 'none'"`,
|
||||
);
|
||||
await expect(
|
||||
service.getUserInfo(mockCredentials.service()),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"User info not available for principal type 'service'"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
BackstageCredentials,
|
||||
BackstageNonePrincipal,
|
||||
BackstageServicePrincipal,
|
||||
BackstageUserInfo,
|
||||
BackstageUserPrincipal,
|
||||
UserInfoService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { InputError } from '@backstage/errors';
|
||||
|
||||
/** @internal */
|
||||
export class MockUserInfoService implements UserInfoService {
|
||||
private readonly customInfo: Partial<BackstageUserInfo>;
|
||||
|
||||
constructor(customInfo?: Partial<BackstageUserInfo>) {
|
||||
this.customInfo = customInfo ?? {};
|
||||
}
|
||||
|
||||
async getUserInfo(
|
||||
credentials: BackstageCredentials,
|
||||
): Promise<BackstageUserInfo> {
|
||||
const principal = credentials.principal as
|
||||
| BackstageUserPrincipal
|
||||
| BackstageServicePrincipal
|
||||
| BackstageNonePrincipal;
|
||||
|
||||
if (principal.type !== 'user') {
|
||||
throw new InputError(
|
||||
`User info not available for principal type '${principal.type}'`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
userEntityRef: principal.userEntityRef,
|
||||
ownershipEntityRefs: [principal.userEntityRef],
|
||||
...this.customInfo,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,18 @@ describe('mockCredentials', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a mocked credentials object for a limited user principal', () => {
|
||||
expect(mockCredentials.limitedUser()).toEqual({
|
||||
$$type: '@backstage/BackstageCredentials',
|
||||
principal: { type: 'user', userEntityRef: 'user:default/mock' },
|
||||
});
|
||||
|
||||
expect(mockCredentials.limitedUser('user:default/other')).toEqual({
|
||||
$$type: '@backstage/BackstageCredentials',
|
||||
principal: { type: 'user', userEntityRef: 'user:default/other' },
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a mocked credentials object for a service principal', () => {
|
||||
expect(mockCredentials.service()).toEqual({
|
||||
$$type: '@backstage/BackstageCredentials',
|
||||
@@ -68,6 +80,22 @@ describe('mockCredentials', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('creates limited user tokens and headers', () => {
|
||||
expect(mockCredentials.limitedUser.token('user:default/other')).toBe(
|
||||
'mock-limited-user-token:{"sub":"user:default/other"}',
|
||||
);
|
||||
expect(mockCredentials.limitedUser.invalidToken()).toBe(
|
||||
'mock-invalid-limited-user-token',
|
||||
);
|
||||
|
||||
expect(mockCredentials.limitedUser.cookie('user:default/other')).toBe(
|
||||
'backstage-auth=mock-limited-user-token:{"sub":"user:default/other"}',
|
||||
);
|
||||
expect(mockCredentials.limitedUser.invalidCookie()).toBe(
|
||||
'backstage-auth=mock-invalid-limited-user-token',
|
||||
);
|
||||
});
|
||||
|
||||
it('creates service tokens and headers', () => {
|
||||
expect(mockCredentials.service.token()).toBe('mock-service-token');
|
||||
expect(
|
||||
|
||||
@@ -24,10 +24,18 @@ import {
|
||||
export const DEFAULT_MOCK_USER_ENTITY_REF = 'user:default/mock';
|
||||
export const DEFAULT_MOCK_SERVICE_SUBJECT = 'external:test-service';
|
||||
|
||||
export const MOCK_AUTH_COOKIE = 'backstage-auth';
|
||||
|
||||
export const MOCK_NONE_TOKEN = 'mock-none-token';
|
||||
|
||||
export const MOCK_USER_TOKEN = 'mock-user-token';
|
||||
export const MOCK_USER_TOKEN_PREFIX = 'mock-user-token:';
|
||||
export const MOCK_INVALID_USER_TOKEN = 'mock-invalid-user-token';
|
||||
|
||||
export const MOCK_USER_LIMITED_TOKEN_PREFIX = 'mock-limited-user-token:';
|
||||
export const MOCK_INVALID_USER_LIMITED_TOKEN =
|
||||
'mock-invalid-limited-user-token';
|
||||
|
||||
export const MOCK_SERVICE_TOKEN = 'mock-service-token';
|
||||
export const MOCK_SERVICE_TOKEN_PREFIX = 'mock-service-token:';
|
||||
export const MOCK_INVALID_SERVICE_TOKEN = 'mock-invalid-service-token';
|
||||
@@ -143,6 +151,54 @@ export namespace mockCredentials {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mocked credentials object for a user principal with limited
|
||||
* access.
|
||||
*
|
||||
* The default user entity reference is 'user:default/mock'.
|
||||
*/
|
||||
export function limitedUser(
|
||||
userEntityRef: string = DEFAULT_MOCK_USER_ENTITY_REF,
|
||||
): BackstageCredentials<BackstageUserPrincipal> {
|
||||
return user(userEntityRef);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilities related to limited user credentials.
|
||||
*/
|
||||
export namespace limitedUser {
|
||||
/**
|
||||
* Creates a mocked limited user token. If a payload is provided it will be
|
||||
* encoded into the token and forwarded to the credentials object when
|
||||
* authenticated by the mock auth service.
|
||||
*/
|
||||
export function token(
|
||||
userEntityRef: string = DEFAULT_MOCK_USER_ENTITY_REF,
|
||||
): string {
|
||||
validateUserEntityRef(userEntityRef);
|
||||
return `${MOCK_USER_LIMITED_TOKEN_PREFIX}${JSON.stringify({
|
||||
sub: userEntityRef,
|
||||
} satisfies UserTokenPayload)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an authorization header with a mocked limited user token. If a
|
||||
* payload is provided it will be encoded into the token and forwarded to
|
||||
* the credentials object when authenticated by the mock auth service.
|
||||
*/
|
||||
export function cookie(userEntityRef?: string): string {
|
||||
return `${MOCK_AUTH_COOKIE}=${token(userEntityRef)}`;
|
||||
}
|
||||
|
||||
export function invalidToken(): string {
|
||||
return MOCK_INVALID_USER_LIMITED_TOKEN;
|
||||
}
|
||||
|
||||
export function invalidCookie(): string {
|
||||
return `${MOCK_AUTH_COOKIE}=${invalidToken()}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mocked credentials object for a service principal.
|
||||
*
|
||||
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
DiscoveryService,
|
||||
HttpAuthService,
|
||||
BackstageCredentials,
|
||||
BackstageUserInfo,
|
||||
UserInfoService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
cacheServiceFactory,
|
||||
@@ -49,6 +51,7 @@ import { MockRootLoggerService } from './MockRootLoggerService';
|
||||
import { MockAuthService } from './MockAuthService';
|
||||
import { MockHttpAuthService } from './MockHttpAuthService';
|
||||
import { mockCredentials } from './mockCredentials';
|
||||
import { MockUserInfoService } from './MockUserInfoService';
|
||||
|
||||
/** @internal */
|
||||
function simpleFactory<
|
||||
@@ -199,9 +202,11 @@ export namespace mockServices {
|
||||
});
|
||||
export const mock = simpleMock(coreServices.auth, () => ({
|
||||
authenticate: jest.fn(),
|
||||
getNoneCredentials: jest.fn(),
|
||||
getOwnServiceCredentials: jest.fn(),
|
||||
isPrincipal: jest.fn() as any,
|
||||
getPluginRequestToken: jest.fn(),
|
||||
getLimitedUserToken: jest.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -272,6 +277,37 @@ export namespace mockServices {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock implementation of the `UserInfoService`.
|
||||
*
|
||||
* By default it extracts the user's entity ref from a user principal and
|
||||
* returns that as the only ownership entity ref, but this can be overridden
|
||||
* by passing in a custom set of user info.
|
||||
*/
|
||||
export function userInfo(
|
||||
customInfo?: Partial<BackstageUserInfo>,
|
||||
): UserInfoService {
|
||||
return new MockUserInfoService(customInfo);
|
||||
}
|
||||
export namespace userInfo {
|
||||
/**
|
||||
* Creates a mock service factory for the `UserInfoService`.
|
||||
*
|
||||
* By default it extracts the user's entity ref from a user principal and
|
||||
* returns that as the only ownership entity ref.
|
||||
*/
|
||||
export const factory = createServiceFactory({
|
||||
service: coreServices.userInfo,
|
||||
deps: {},
|
||||
factory() {
|
||||
return new MockUserInfoService();
|
||||
},
|
||||
});
|
||||
export const mock = simpleMock(coreServices.userInfo, () => ({
|
||||
getUserInfo: jest.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
// TODO(Rugvip): Not all core services have implementations available here yet.
|
||||
// some may need a bit more refactoring for it to be simpler to
|
||||
// re-implement functioning mock versions here.
|
||||
@@ -284,12 +320,14 @@ export namespace mockServices {
|
||||
withOptions: jest.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
export namespace database {
|
||||
export const factory = databaseServiceFactory;
|
||||
export const mock = simpleMock(coreServices.database, () => ({
|
||||
getClient: jest.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
export namespace httpRouter {
|
||||
export const factory = httpRouterServiceFactory;
|
||||
export const mock = simpleMock(coreServices.httpRouter, () => ({
|
||||
@@ -297,12 +335,14 @@ export namespace mockServices {
|
||||
addAuthPolicy: jest.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
export namespace rootHttpRouter {
|
||||
export const factory = rootHttpRouterServiceFactory;
|
||||
export const mock = simpleMock(coreServices.rootHttpRouter, () => ({
|
||||
use: jest.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
export namespace lifecycle {
|
||||
export const factory = lifecycleServiceFactory;
|
||||
export const mock = simpleMock(coreServices.lifecycle, () => ({
|
||||
@@ -310,6 +350,7 @@ export namespace mockServices {
|
||||
addStartupHook: jest.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
export namespace logger {
|
||||
export const factory = loggerServiceFactory;
|
||||
export const mock = simpleMock(coreServices.logger, () => ({
|
||||
@@ -320,6 +361,7 @@ export namespace mockServices {
|
||||
warn: jest.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
export namespace permissions {
|
||||
export const factory = permissionsServiceFactory;
|
||||
export const mock = simpleMock(coreServices.permissions, () => ({
|
||||
@@ -327,6 +369,7 @@ export namespace mockServices {
|
||||
authorizeConditional: jest.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
export namespace rootLifecycle {
|
||||
export const factory = rootLifecycleServiceFactory;
|
||||
export const mock = simpleMock(coreServices.rootLifecycle, () => ({
|
||||
@@ -334,6 +377,7 @@ export namespace mockServices {
|
||||
addStartupHook: jest.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
export namespace scheduler {
|
||||
export const factory = schedulerServiceFactory;
|
||||
export const mock = simpleMock(coreServices.scheduler, () => ({
|
||||
@@ -343,6 +387,7 @@ export namespace mockServices {
|
||||
triggerTask: jest.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
export namespace urlReader {
|
||||
export const factory = urlReaderServiceFactory;
|
||||
export const mock = simpleMock(coreServices.urlReader, () => ({
|
||||
|
||||
@@ -80,6 +80,7 @@ export const defaultServiceFactories = [
|
||||
mockServices.rootLogger.factory(),
|
||||
mockServices.scheduler.factory(),
|
||||
mockServices.tokenManager.factory(),
|
||||
mockServices.userInfo.factory(),
|
||||
mockServices.urlReader.factory(),
|
||||
];
|
||||
|
||||
|
||||
@@ -1,5 +1,65 @@
|
||||
# example-backend
|
||||
|
||||
## 0.2.93-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/plugin-events-backend@0.3.0-next.0
|
||||
- @backstage/plugin-events-node@0.3.0-next.0
|
||||
- @backstage/plugin-linguist-backend@0.5.10-next.0
|
||||
- @backstage/backend-common@0.21.3-next.0
|
||||
- @backstage/plugin-auth-node@0.4.8-next.0
|
||||
- @backstage/plugin-lighthouse-backend@0.4.5-next.0
|
||||
- @backstage/plugin-search-backend-module-elasticsearch@1.3.16-next.0
|
||||
- @backstage/plugin-search-backend-module-pg@0.5.22-next.0
|
||||
- @backstage/plugin-playlist-backend@0.3.17-next.0
|
||||
- @backstage/plugin-code-coverage-backend@0.2.27-next.0
|
||||
- @backstage/plugin-entity-feedback-backend@0.2.10-next.0
|
||||
- @backstage/plugin-catalog-backend@1.18.0-next.0
|
||||
- @backstage/plugin-auth-backend@0.22.0-next.0
|
||||
- @backstage/plugin-jenkins-backend@0.4.0-next.0
|
||||
- @backstage/plugin-azure-devops-backend@0.6.0-next.0
|
||||
- @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.14-next.0
|
||||
- @backstage/plugin-scaffolder-backend-module-gitlab@0.2.16-next.0
|
||||
- @backstage/plugin-scaffolder-backend-module-rails@0.4.30-next.0
|
||||
- @backstage/plugin-scaffolder-backend@1.22.0-next.0
|
||||
- @backstage/plugin-permission-common@0.7.13-next.0
|
||||
- @backstage/plugin-search-backend-module-techdocs@0.1.17-next.0
|
||||
- @backstage/plugin-search-backend-module-catalog@0.1.17-next.0
|
||||
- @backstage/plugin-search-backend-module-explore@0.1.17-next.0
|
||||
- @backstage/plugin-catalog-node@1.8.0-next.0
|
||||
- @backstage/plugin-kubernetes-backend@0.16.0-next.0
|
||||
- @backstage/plugin-adr-backend@0.4.10-next.0
|
||||
- @backstage/plugin-proxy-backend@0.4.11-next.0
|
||||
- @backstage/backend-tasks@0.5.18-next.0
|
||||
- @backstage/plugin-search-backend-node@1.2.17-next.0
|
||||
- @backstage/plugin-signals-backend@0.0.4-next.0
|
||||
- @backstage/plugin-signals-node@0.0.4-next.0
|
||||
- @backstage/plugin-tech-insights-backend@0.5.27-next.0
|
||||
- @backstage/plugin-search-backend@1.5.3-next.0
|
||||
- @backstage/plugin-devtools-backend@0.3.0-next.0
|
||||
- @backstage/plugin-permission-node@0.7.24-next.0
|
||||
- @backstage/plugin-tech-insights-node@0.5.0-next.0
|
||||
- @backstage/plugin-badges-backend@0.3.10-next.0
|
||||
- @backstage/plugin-permission-backend@0.5.36-next.0
|
||||
- @backstage/plugin-app-backend@0.3.61-next.0
|
||||
- @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.10-next.0
|
||||
- @backstage/plugin-explore-backend@0.0.23-next.0
|
||||
- @backstage/plugin-rollbar-backend@0.1.58-next.0
|
||||
- @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.45-next.0
|
||||
- @backstage/plugin-techdocs-backend@1.9.6-next.0
|
||||
- @backstage/plugin-kafka-backend@0.3.11-next.0
|
||||
- @backstage/plugin-nomad-backend@0.1.15-next.0
|
||||
- @backstage/plugin-todo-backend@0.3.11-next.0
|
||||
- @backstage/plugin-catalog-backend-module-unprocessed@0.3.10-next.0
|
||||
- example-app@0.2.93-next.0
|
||||
- @backstage/catalog-client@1.6.1-next.0
|
||||
- @backstage/catalog-model@1.4.5-next.0
|
||||
- @backstage/config@1.1.2-next.0
|
||||
- @backstage/integration@1.9.1-next.0
|
||||
- @backstage/plugin-azure-sites-common@0.1.3-next.0
|
||||
|
||||
## 0.2.92
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-backend",
|
||||
"version": "0.2.92",
|
||||
"version": "0.2.93-next.0",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -72,7 +72,6 @@
|
||||
"@backstage/plugin-search-backend-module-pg": "workspace:^",
|
||||
"@backstage/plugin-search-backend-module-techdocs": "workspace:^",
|
||||
"@backstage/plugin-search-backend-node": "workspace:^",
|
||||
"@backstage/plugin-search-common": "workspace:^",
|
||||
"@backstage/plugin-signals-backend": "workspace:^",
|
||||
"@backstage/plugin-signals-node": "workspace:^",
|
||||
"@backstage/plugin-tech-insights-backend": "workspace:^",
|
||||
|
||||
@@ -25,5 +25,6 @@ export default async function createPlugin(
|
||||
logger: env.logger,
|
||||
config: env.config,
|
||||
permissions: env.permissions,
|
||||
discovery: env.discovery,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ export default async function createPlugin(
|
||||
config: env.config,
|
||||
catalogApi,
|
||||
permissions: env.permissions,
|
||||
discovery: env.discovery,
|
||||
}).build();
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -23,9 +23,9 @@ import { ElasticSearchSearchEngine } from '@backstage/plugin-search-backend-modu
|
||||
import { PgSearchEngine } from '@backstage/plugin-search-backend-module-pg';
|
||||
import {
|
||||
IndexBuilder,
|
||||
SearchEngine,
|
||||
LunrSearchEngine,
|
||||
} from '@backstage/plugin-search-backend-node';
|
||||
import { SearchEngine } from '@backstage/plugin-search-common';
|
||||
import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-search-backend-module-techdocs';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
@@ -117,6 +117,7 @@ export default async function createPlugin(
|
||||
return await createRouter({
|
||||
engine: indexBuilder.getSearchEngine(),
|
||||
types: indexBuilder.getDocumentTypes(),
|
||||
discovery: env.discovery,
|
||||
permissions: env.permissions,
|
||||
config: env.config,
|
||||
logger: env.logger,
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# @backstage/catalog-client
|
||||
|
||||
## 1.6.1-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/errors@1.2.4-next.0
|
||||
- @backstage/catalog-model@1.4.5-next.0
|
||||
|
||||
## 1.6.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/catalog-client",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1-next.0",
|
||||
"description": "An isomorphic client for the catalog backend",
|
||||
"backstage": {
|
||||
"role": "common-library"
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# @backstage/catalog-model
|
||||
|
||||
## 1.4.5-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/errors@1.2.4-next.0
|
||||
- @backstage/types@1.1.1
|
||||
|
||||
## 1.4.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -57,3 +57,17 @@ spec:
|
||||
displayName: Guest User
|
||||
email: guest@example.com
|
||||
memberOf: [team-a]
|
||||
---
|
||||
# This user is added as an example, to make it more easy for the "Guest"
|
||||
# sign-in option to demonstrate some entities being owned. In a regular org,
|
||||
# a guest user would probably not be registered like this.
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: User
|
||||
metadata:
|
||||
name: guest
|
||||
namespace: development
|
||||
spec:
|
||||
profile:
|
||||
displayName: Guest User
|
||||
email: guest@example.com
|
||||
memberOf: [group:default/team-a]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/catalog-model",
|
||||
"version": "1.4.4",
|
||||
"version": "1.4.5-next.0",
|
||||
"description": "Types and validators that help describe the model of a Backstage Catalog",
|
||||
"backstage": {
|
||||
"role": "common-library"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user