Merge branch 'backstage:master' into master

This commit is contained in:
Ambrish R
2025-04-22 10:35:56 +05:30
committed by GitHub
199 changed files with 1252 additions and 17384 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/integration': patch
---
Added missing `organizations` property to `azure` section in `config.d.ts` file
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-gitlab': patch
---
If the commit action is not `create` log a more appropriate error message to the end user advising that the files they're trying to modify might not exist
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/canon': patch
---
Fix Canon missing dependencies
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend-module-github-provider': patch
---
Added missing types package
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-unprocessed': minor
---
**BREAKING** Removed support for the legacy backend and removed references to `@backstage/backend-common`, please [migrate to the new backend system](https://backstage.io/docs/backend-system/building-plugins-and-modules/migrating)
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Fix EntityPicker field to render description as markdown, matching other form components in the system.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-events-backend-module-gitlab': patch
---
Adds support for `object_kind` field with priority over `event_name` on Gitlab webhook event types
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-auth-backend': minor
---
**BREAKING**: Removed support for the old backend system, and removed all deprecated exports.
If you were using one of the deprecated imports from this package, you will have to follow the instructions in their respective deprecation notices before upgrading. Most of the general utilities are available from `@backstage/plugin-auth-node`, and the specific auth providers are available from dedicated packages such as for example `@backstage/plugin-auth-backend-module-github-provider`. See [the auth docs](https://backstage.io/docs/auth/) for specific instructions.
+203
View File
@@ -0,0 +1,203 @@
{
"mode": "pre",
"tag": "next",
"initialVersions": {
"example-app": "0.2.108",
"@backstage/app-defaults": "1.6.1",
"example-app-next": "0.0.22",
"app-next-example-plugin": "0.0.22",
"example-backend": "0.0.37",
"@backstage/backend-app-api": "1.2.2",
"@backstage/backend-defaults": "0.9.0",
"@backstage/backend-dev-utils": "0.1.5",
"@backstage/backend-dynamic-feature-service": "0.6.2",
"@backstage/backend-openapi-utils": "0.5.2",
"@backstage/backend-plugin-api": "1.3.0",
"@backstage/backend-test-utils": "1.4.0",
"@backstage/canon": "0.3.0",
"@backstage/catalog-client": "1.9.1",
"@backstage/catalog-model": "1.7.3",
"@backstage/cli": "0.32.0",
"@backstage/cli-common": "0.1.15",
"@backstage/cli-node": "0.2.13",
"@backstage/codemods": "0.1.52",
"@backstage/config": "1.3.2",
"@backstage/config-loader": "1.10.0",
"@backstage/core-app-api": "1.16.1",
"@backstage/core-compat-api": "0.4.1",
"@backstage/core-components": "0.17.1",
"@backstage/core-plugin-api": "1.10.6",
"@backstage/create-app": "0.6.1",
"@backstage/dev-utils": "1.1.9",
"e2e-test": "0.2.27",
"@backstage/e2e-test-utils": "0.1.1",
"@backstage/errors": "1.2.7",
"@backstage/eslint-plugin": "0.1.10",
"@backstage/frontend-app-api": "0.11.1",
"@backstage/frontend-defaults": "0.2.1",
"@backstage/frontend-dynamic-feature-loader": "0.1.0",
"@internal/frontend": "0.0.8",
"@backstage/frontend-plugin-api": "0.10.1",
"@backstage/frontend-test-utils": "0.3.1",
"@backstage/integration": "1.16.3",
"@backstage/integration-aws-node": "0.1.15",
"@backstage/integration-react": "1.2.6",
"@internal/opaque": "0.0.1",
"@backstage/release-manifests": "0.0.12",
"@backstage/repo-tools": "0.13.2",
"@internal/scaffolder": "0.0.8",
"@techdocs/cli": "1.9.2",
"techdocs-cli-embedded-app": "0.2.107",
"@backstage/test-utils": "1.7.7",
"@backstage/theme": "0.6.5",
"@backstage/types": "1.2.1",
"@backstage/version-bridge": "1.0.11",
"yarn-plugin-backstage": "0.0.4",
"@backstage/plugin-api-docs": "0.12.6",
"@backstage/plugin-api-docs-module-protoc-gen-doc": "0.1.10",
"@backstage/plugin-app": "0.1.8",
"@backstage/plugin-app-backend": "0.5.1",
"@backstage/plugin-app-node": "0.1.32",
"@backstage/plugin-app-visualizer": "0.1.18",
"@backstage/plugin-auth-backend": "0.24.5",
"@backstage/plugin-auth-backend-module-atlassian-provider": "0.4.2",
"@backstage/plugin-auth-backend-module-auth0-provider": "0.2.2",
"@backstage/plugin-auth-backend-module-aws-alb-provider": "0.4.2",
"@backstage/plugin-auth-backend-module-azure-easyauth-provider": "0.2.7",
"@backstage/plugin-auth-backend-module-bitbucket-provider": "0.3.2",
"@backstage/plugin-auth-backend-module-bitbucket-server-provider": "0.2.2",
"@backstage/plugin-auth-backend-module-cloudflare-access-provider": "0.4.2",
"@backstage/plugin-auth-backend-module-gcp-iap-provider": "0.4.2",
"@backstage/plugin-auth-backend-module-github-provider": "0.3.2",
"@backstage/plugin-auth-backend-module-gitlab-provider": "0.3.2",
"@backstage/plugin-auth-backend-module-google-provider": "0.3.2",
"@backstage/plugin-auth-backend-module-guest-provider": "0.2.7",
"@backstage/plugin-auth-backend-module-microsoft-provider": "0.3.2",
"@backstage/plugin-auth-backend-module-oauth2-provider": "0.4.2",
"@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "0.2.7",
"@backstage/plugin-auth-backend-module-oidc-provider": "0.4.2",
"@backstage/plugin-auth-backend-module-okta-provider": "0.2.2",
"@backstage/plugin-auth-backend-module-onelogin-provider": "0.3.2",
"@backstage/plugin-auth-backend-module-pinniped-provider": "0.3.2",
"@backstage/plugin-auth-backend-module-vmware-cloud-provider": "0.5.2",
"@backstage/plugin-auth-node": "0.6.2",
"@backstage/plugin-auth-react": "0.1.14",
"@backstage/plugin-bitbucket-cloud-common": "0.2.29",
"@backstage/plugin-catalog": "1.29.0",
"@backstage/plugin-catalog-backend": "1.32.1",
"@backstage/plugin-catalog-backend-module-aws": "0.4.10",
"@backstage/plugin-catalog-backend-module-azure": "0.3.4",
"@backstage/plugin-catalog-backend-module-backstage-openapi": "0.5.1",
"@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.4.7",
"@backstage/plugin-catalog-backend-module-bitbucket-server": "0.4.0",
"@backstage/plugin-catalog-backend-module-gcp": "0.3.7",
"@backstage/plugin-catalog-backend-module-gerrit": "0.3.1",
"@backstage/plugin-catalog-backend-module-github": "0.8.0",
"@backstage/plugin-catalog-backend-module-github-org": "0.3.9",
"@backstage/plugin-catalog-backend-module-gitlab": "0.6.5",
"@backstage/plugin-catalog-backend-module-gitlab-org": "0.2.8",
"@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.6.5",
"@backstage/plugin-catalog-backend-module-ldap": "0.11.4",
"@backstage/plugin-catalog-backend-module-logs": "0.1.9",
"@backstage/plugin-catalog-backend-module-msgraph": "0.6.9",
"@backstage/plugin-catalog-backend-module-openapi": "0.2.9",
"@backstage/plugin-catalog-backend-module-puppetdb": "0.2.9",
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "0.2.7",
"@backstage/plugin-catalog-backend-module-unprocessed": "0.5.7",
"@backstage/plugin-catalog-common": "1.1.3",
"@backstage/plugin-catalog-graph": "0.4.18",
"@backstage/plugin-catalog-import": "0.12.13",
"@backstage/plugin-catalog-node": "1.16.3",
"@backstage/plugin-catalog-react": "1.17.0",
"@backstage/plugin-catalog-unprocessed-entities": "0.2.16",
"@backstage/plugin-catalog-unprocessed-entities-common": "0.0.7",
"@backstage/plugin-config-schema": "0.1.67",
"@backstage/plugin-devtools": "0.1.26",
"@backstage/plugin-devtools-backend": "0.5.4",
"@backstage/plugin-devtools-common": "0.1.15",
"@backstage/plugin-events-backend": "0.5.1",
"@backstage/plugin-events-backend-module-aws-sqs": "0.4.10",
"@backstage/plugin-events-backend-module-azure": "0.2.19",
"@backstage/plugin-events-backend-module-bitbucket-cloud": "0.2.19",
"@backstage/plugin-events-backend-module-bitbucket-server": "0.1.0",
"@backstage/plugin-events-backend-module-gerrit": "0.2.19",
"@backstage/plugin-events-backend-module-github": "0.3.0",
"@backstage/plugin-events-backend-module-gitlab": "0.3.0",
"@backstage/plugin-events-backend-test-utils": "0.1.43",
"@backstage/plugin-events-node": "0.4.10",
"@internal/plugin-todo-list": "1.0.38",
"@internal/plugin-todo-list-backend": "1.0.38",
"@internal/plugin-todo-list-common": "1.0.24",
"@backstage/plugin-gateway-backend": "1.0.0",
"@backstage/plugin-home": "0.8.7",
"@backstage/plugin-home-react": "0.1.25",
"@backstage/plugin-kubernetes": "0.12.6",
"@backstage/plugin-kubernetes-backend": "0.19.5",
"@backstage/plugin-kubernetes-cluster": "0.0.24",
"@backstage/plugin-kubernetes-common": "0.9.4",
"@backstage/plugin-kubernetes-node": "0.2.5",
"@backstage/plugin-kubernetes-react": "0.5.6",
"@backstage/plugin-notifications": "0.5.4",
"@backstage/plugin-notifications-backend": "0.5.5",
"@backstage/plugin-notifications-backend-module-email": "0.3.8",
"@backstage/plugin-notifications-backend-module-slack": "0.1.0",
"@backstage/plugin-notifications-common": "0.0.8",
"@backstage/plugin-notifications-node": "0.2.14",
"@backstage/plugin-org": "0.6.38",
"@backstage/plugin-org-react": "0.1.37",
"@backstage/plugin-permission-backend": "0.6.0",
"@backstage/plugin-permission-backend-module-allow-all-policy": "0.2.7",
"@backstage/plugin-permission-common": "0.8.4",
"@backstage/plugin-permission-node": "0.9.1",
"@backstage/plugin-permission-react": "0.4.33",
"@backstage/plugin-proxy-backend": "0.6.1",
"@backstage/plugin-proxy-node": "0.1.3",
"@backstage/plugin-scaffolder": "1.30.0",
"@backstage/plugin-scaffolder-backend": "1.32.0",
"@backstage/plugin-scaffolder-backend-module-azure": "0.2.8",
"@backstage/plugin-scaffolder-backend-module-bitbucket": "0.3.9",
"@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "0.2.8",
"@backstage/plugin-scaffolder-backend-module-bitbucket-server": "0.2.8",
"@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "0.3.8",
"@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.3.9",
"@backstage/plugin-scaffolder-backend-module-gcp": "0.2.8",
"@backstage/plugin-scaffolder-backend-module-gerrit": "0.2.8",
"@backstage/plugin-scaffolder-backend-module-gitea": "0.2.8",
"@backstage/plugin-scaffolder-backend-module-github": "0.7.0",
"@backstage/plugin-scaffolder-backend-module-gitlab": "0.9.0",
"@backstage/plugin-scaffolder-backend-module-notifications": "0.1.9",
"@backstage/plugin-scaffolder-backend-module-rails": "0.5.8",
"@backstage/plugin-scaffolder-backend-module-sentry": "0.2.8",
"@backstage/plugin-scaffolder-backend-module-yeoman": "0.4.9",
"@backstage/plugin-scaffolder-common": "1.5.10",
"@backstage/plugin-scaffolder-node": "0.8.1",
"@backstage/plugin-scaffolder-node-test-utils": "0.2.1",
"@backstage/plugin-scaffolder-react": "1.15.0",
"@backstage/plugin-search": "1.4.25",
"@backstage/plugin-search-backend": "2.0.1",
"@backstage/plugin-search-backend-module-catalog": "0.3.3",
"@backstage/plugin-search-backend-module-elasticsearch": "1.7.1",
"@backstage/plugin-search-backend-module-explore": "0.3.1",
"@backstage/plugin-search-backend-module-pg": "0.5.43",
"@backstage/plugin-search-backend-module-stack-overflow-collator": "0.3.8",
"@backstage/plugin-search-backend-module-techdocs": "0.4.1",
"@backstage/plugin-search-backend-node": "1.3.10",
"@backstage/plugin-search-common": "1.2.17",
"@backstage/plugin-search-react": "1.8.8",
"@backstage/plugin-signals": "0.0.18",
"@backstage/plugin-signals-backend": "0.3.3",
"@backstage/plugin-signals-node": "0.1.19",
"@backstage/plugin-signals-react": "0.0.12",
"@backstage/plugin-techdocs": "1.12.5",
"@backstage/plugin-techdocs-addons-test-utils": "1.0.47",
"@backstage/plugin-techdocs-backend": "2.0.1",
"@backstage/plugin-techdocs-common": "0.1.0",
"@backstage/plugin-techdocs-module-addons-contrib": "1.1.23",
"@backstage/plugin-techdocs-node": "1.13.2",
"@backstage/plugin-techdocs-react": "1.2.16",
"@backstage/plugin-user-settings": "0.8.21",
"@backstage/plugin-user-settings-backend": "0.3.1",
"@backstage/plugin-user-settings-common": "0.0.1"
},
"changesets": []
}
+26
View File
@@ -0,0 +1,26 @@
---
'@backstage/plugin-catalog-react': minor
---
Added EntityOrderFilter to sort entities by different fields/columns. This new filter allows users to specify the order in which entities are displayed in the catalog.
Example usage:
```ts
import {
EntityOrderFilter,
useEntityList,
} from '@backstage/plugin-catalog-react';
// ...
const { updateFilters } = useEntityList();
// ...
updateFilters({
order: new EntityOrderFilter([
{
field: 'metadata.name',
order: 'desc',
},
]),
});
```
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-scaffolder-backend': patch
'@backstage/plugin-scaffolder-react': patch
'@backstage/plugin-scaffolder': patch
---
Fixing a bug where the name for `templatingExtensions` was incorrectly set to `templateExtensions`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-permission-backend': minor
---
**BREAKING** Removed support for the legacy backend system, please [migrate to the new backend system](https://backstage.io/docs/backend-system/building-backends/migrating)
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/catalog-client': minor
'@backstage/plugin-catalog-react': minor
'@backstage/plugin-catalog-node': minor
---
Add `getLocations` method to `CatalogApi` and `CatalogClient`. This method calls the [`GET /locations`](https://backstage.io/docs/features/software-catalog/software-catalog-api/#get-locations) endpoint from the catalog backend.
+2 -5
View File
@@ -231,11 +231,8 @@ jobs:
run: yarn docusaurus docs:version stable
working-directory: microsite
- name: clear API reference
run: rm -r docs/reference
- name: clear OpenAPI reference
run: find ./docs -name '*.api.mdx' -type f -delete
- name: clear generated docs
run: git clean -fdx docs/
# Next docs
- name: checkout master
+2 -5
View File
@@ -242,11 +242,8 @@ jobs:
run: yarn docusaurus docs:version stable
working-directory: microsite
- name: clear API reference
run: rm -r docs/reference
- name: clear OpenAPI reference
run: find ./docs -name '*.api.mdx' -type f -delete
- name: clear generated docs
run: git clean -fdx docs/
- name: build API reference
run: yarn build:api-docs
+4 -4
View File
@@ -108,7 +108,7 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/
| [Beez Innovation Labs Pvt. Ltd](https://www.beezlabs.com/) | [Karthikeyan Venkatesan](https://github.com/karthikeyan23) | Developer portal with software catalog, scaffolding, tech docs, templates, and infra. |
| [Agorapulse](https://www.agorapulse.com/) | [@jvdrean](https://github.com/jvdrean) | Developer portal with software catalog, documentation, monitoring, runbooks, tech radar and more. |
| [Wistia](https://wistia.com/) | [@qrush](https://github.com/qrush), [@okize](https://github.com/okize) | Internal Developer Portal, service catalog, tech docs and more |
| [SIX](https://www.six-group.com/) | [@jbadeau](https://github.com/jbadeau), [@tomassatka](https://github.com/tomassatka) | Internal DevOps portal hosting our software and dataset catalog, as well as custom plugins for observability, service virtualization, deployments, incident managment and quality metrics. |
| [SIX](https://www.six-group.com/) | [@jbadeau](https://github.com/jbadeau), [@tomassatka](https://github.com/tomassatka) | Internal DevOps portal hosting our software and dataset catalog, as well as custom plugins for observability, service virtualization, deployments, incident management and quality metrics. |
| [Raiffeisen Bank International](https://www.rbinternational.com/) | [Daniel Baumgartner](https://github.com/dabarbi) | From developers for developers: software catalog, techdocs and heavy use of scaffolder to drive reuse on engineering level forward. Part of inner source initiative. Multi national setup coming. |
| [Spread Group](https://www.spreadgroup.com/) | [Luna Stadler](https://github.com/heyLu), [Iván González](https://github.com/ivangonzalezacuna) | Internal Developer Portal, an overview of all running software, architecture documentation and more; replacing and unifying a variety of internal tools. |
| [RD Station](https://rdstation.com) | [Rogerio Angeliski](https://github.com/angeliski), [Paula Assis](https://github.com/paulassis), [Guilherme Eric](https://github.com/guilhermeeric), [Daniela Adamatti](https://github.com/daniadamatti), [Luana Negreiros](https://github.com/luananegreiros) | Developer portal, scaffolding, services catalog. We are looking to centralize automations and information for the whole engineering team . |
@@ -206,7 +206,7 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/
| [Paraná Banco](https://site.paranabanco.com.br/) | [Joao Antunes](mailto:joaopma@pbtech.net.br) | Internal software catalog, documentation and ownership, improve communication, democratize documentation and knowledge sharing, and coordinate the software lifecycle; all in service of a best-in-class developer experience. |
| [Stone](https://stone.com.br/) | [Levy Fialho](mailto:lfialho@stone.com.br) | We're using Backstage as our Credit Team Developer Portal and microservices catalog for mapping ownership. We are also using mkdocs for microservices documentation. |
| [REI](https://www.rei.com/) | [Jen Evans](mailto:jenevan@rei.com) | Developer portal focused on an enterprise-wide app catalog to track ownership and surface APIs. |
| [next](https://next.me) | [Devan Jeronimo Nack](mailto:devan.j.nack@next.me), [Everson Crusara](mailto:everson.crusara@next.me), [Thiago Carneiro da Silva](mailto:thiagoc.silva@next.me) | We are building our Internal Developer Portal using Backstage to improve developer's experience by centralizing our services catalog and identifing microservices' ownership. Also we are going to improve Technical Documentation and speed up development using software templates to help squads in creation and deployment of new microservices. |
| [next](https://next.me) | [Devan Jeronimo Nack](mailto:devan.j.nack@next.me), [Everson Crusara](mailto:everson.crusara@next.me), [Thiago Carneiro da Silva](mailto:thiagoc.silva@next.me) | We are building our Internal Developer Portal using Backstage to improve developer's experience by centralizing our services catalog and identifying microservices' ownership. Also we are going to improve Technical Documentation and speed up development using software templates to help squads in creation and deployment of new microservices. |
| [Vipps](https://vipps.no) | [Martin Ehrnst](https://github.com/ehrnst) | Vipps use backstage for our service catalog, documentation, and developer portal. Using templates we are able to simplify the developer experience when deploying new services to our platform. |
| [Ferrovial](https://ferrovial.com) | [Jose Luis Rosado](mailto:jlrosado@ferrovial.com) | Backstage is helping us to improve and acelerate dev experience helping teams to quickly find technical documentation, infrastructure templates, pipelines, software components and quickstarters that have been developed by our squads in a inner source friendly environment. |
| [Inter&Co](https://bancointer.com.br) | [Arnaud Lanna](https://github.com/arnaudlanna), [Adriano Silva](https://github.com/adrianovss), [Bruno Grossi](https://github.com/begrossi) | We're using Backstage as our internal Developer Portal to catalog and collect repositories and microservices pieces of information like ownership, deployment time, and documentation. |
@@ -229,7 +229,7 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/
| [Cazoo](https://www.cazoo.co.uk/) | [Abz Mungul](https://www.linkedin.com/in/abzmungul/), [Scott Edwards](https://www.linkedin.com/in/scott-edwards-tech/) | We're assessing Backstage as our developer platform at Cazoo with a focus on reducing cognitive load for our engineers. We're currently aiming for 3 outcomes: creating visibility into service ownership across teams, improving the discoverability of event schemas and relationships, and improving the discoverability of technical documentation and best practices. |
| [Gumtree](https://www.gumtree.com.au) | [Kumar Gaurav](https://www.linkedin.com/in/kumargaurav517) | We are starting to use it as a single place to find all component information in a distributed architecture. |
| [N26](https://n26.com) | [Alexei Timofti](https://www.linkedin.com/in/alexeitimofti) | We use Backstage for our service catalog and are actively looking into adopting other plugins like TechDocs, TechInsights and Software Templates. |
| [The LEGO Group](https://www.lego.com) | [Waqas Ali](https://www.linkedin.com/in/waqasali47) | We are building our internal develper portal on top of Backstage. |
| [The LEGO Group](https://www.lego.com) | [Waqas Ali](https://www.linkedin.com/in/waqasali47) | We are building our internal developer portal on top of Backstage. |
| [CORS.gmbh](https://www.cors.gmbh) | [@dpfaffenbauer](https://github.com/dpfaffenbauer) | Developer Portal for our Projects we develop for our Customers and Hosting them On Kubernetes. |
| [Comcast](https://comcast.github.io/) | [Ryan Emerle](https://github.com/remerle) | Developer portal enabling discovery of products, services, and documentation throughout the enterprise to ultimately reduce friction and improve time-to-market. |
| [Syntasso](https://www.syntasso.io/) | [@syntassodev](https://github.com/syntassodev) | Backstage is used as a optional UI for the [Kratix project](https://kratix.io), a framework for building platforms.
@@ -237,7 +237,7 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/
| [B3](https://www.b3.com.br/) | [Marcos Rodrigues](https://www.linkedin.com/in/marcos-rodrigues-cloud/) | B3 (Brazilian Stock Exchange) will implement a self-service platform focused on software development based on Backstage. The tool will primarily focus on product development and software engineering teams, as well as professionals from other areas who are also involved in software delivery. The new platform will function as a portal where teams can access tools, codes, and templates that have already been tested and are available, serving as building blocks for the development of new solutions. |
| [Porto](https://www.portoseguro.com.br/) | [Camilo Alessandro](https://www.linkedin.com/in/camilo-alessandro/) | Porto (Brazilian insurance company) Centralized developer portal with software catalog, application templates, maturity radar, component taxonomy standardization, and automation in the integration with DevSecOps processes. |
| [Einride](https://github.com/einride) | [@odsod](https://github.com/odsod/) | We use Backstage to create a great developer experience across Einride's entire software stack - from autonomous and electric vehicles to cloud systems - and we've developed a [Backstage Go SDK](https://github.com/einride/backstage-go) for interfacing with Backstage from our Go tooling.
| [Chartboost](https://www.chartboost.com)| [@brucearctor](https://github.com/brucearctor), [@ArtemChekunov](https://github.com/ArtemChekunov) | We are building our internal develper portal on top of Backstage.|
| [Chartboost](https://www.chartboost.com)| [@brucearctor](https://github.com/brucearctor), [@ArtemChekunov](https://github.com/ArtemChekunov) | We are building our internal developer portal on top of Backstage.|
| [Quantum Metric](https://www.quantummetric.com/) | [Eric Irwin](https://www.linkedin.com/in/ericirwin1124/) | Backstage is used within our Developer Experience Platform (DXP) in order to increase self-service, standardization and discoverability across our Engineering teams. |
| [VodafoneZiggo](https://www.vodafoneziggo.nl/) | [Peter Macdonald](https://github.com/Parsifal-M) | We use Backstage as our go-to platform for managing our internal tools and services. With Backstage, we can easily discover and access all the services we need to do our work, whether it's deploying code, managing infrastructure, or accessing documentation. We appreciate the standardized, consistent interface and the ability to easily create custom plugins to integrate with our existing workflows. Overall, Backstage is streamlining our internal operations and helps us work more efficiently as a team. |
| [Volvo Cars](https://www.volvocars.com) | [Martin Wänerskär](https://github.com/martin-wanerskar) | Internal developer portal with intent to unify infrastructure tooling, services, and developer documentation under a single, easy-to-use interface. |
+15
View File
@@ -17,6 +17,7 @@ If you need help, just jump into our [Discord chatroom](https://discord.gg/backs
- [Accessibility](#accessibility)
- [Get Started!](#get-started)
- [Coding Guidelines](#coding-guidelines)
- [Documentation Guidelines](#documentation-guidelines)
- [Package Scripts](#package-scripts)
- [Local configuration](#local-configuration)
- [Creating Changesets](#creating-changesets)
@@ -117,6 +118,20 @@ If there are any updates in `markdown` file please make sure to run `yarn run li
The Backstage development environment does not require any specific editor, but it is intended to be used with one that has built-in linting and type-checking. The development server does not include any checks by default, but they can be enabled using the `--check` flag. Note that using the flag may consume more system resources and slow things down.
## Documentation Guidelines
Contributing to the docs is one of the best ways to start getting involved with Backstage. The documentation site is often the first stop for anyone using or exploring Backstage, so even small improvements can have a big impact!
To help your changes get reviewed and merged smoothly, please keep the following in mind:
- Try to group related updates into a single pull request. For example, if you notice missing admonitions or outdated information in a section, feel free to update all of it together. This makes it easier for maintainers to review your contribution in context.
- We really appreciate contributions that improve clarity or fix outdated information. That said, we generally dont accept changes that are purely stylistic (e.g., rewording a sentence just to tweak the tone or phrasing). If something is **unclear**, **confusing**, or **factually inaccurate**, those are great opportunities to help!
Ready to get started? You can find all the documentation files in the [docs](docs) directory! If you have any questions or need help, feel free to reach out in the [Backstage Discord Docs Channel](https://discord.com/channels/687207715902193673/687994765559463940)
Thank you in advance for your contributions! We really appreciate it. 🙏
## Package Scripts
There are many commands to be found in the root [package.json](https://github.com/backstage/backstage/blob/master/package.json), here are some useful ones:
+2 -2
View File
@@ -357,7 +357,7 @@ Initializing the dynamic feature is just a case of mapping the `DynamicFrontendF
```ts
import { processManifest, getModule } from '@scalprum/core';
// a ID of the module withing module federation container, can be customized, depends on the build
// a ID of the module within module federation container, can be customized, depends on the build
const DEFAULT_MODULE_NAME = 'pluginEntry';
async function loadScalprumFeature({ manifestLocation, name }) {
@@ -680,7 +680,7 @@ const dynamicPluginPlugin = new DynamicRemotePlugin({
version: plugin.version || '0.0.0',
exposedModules: {
// path to the default export of the frontend plugin entry point
// the path should be sourced from the "main" attribute withing package.json
// the path should be sourced from the "main" attribute within package.json
pluginEntry: './src/index.ts',
},
},
@@ -1,5 +1,33 @@
# Releases
## Version 0.3.0
### Main updates
- Add `DataTable` component - ([#29484](https://github.com/backstage/backstage/pull/29484), [#29603](https://github.com/backstage/backstage/pull/29603))
- Add `Select` component - ([#29440](https://github.com/backstage/backstage/pull/29440))
- Add `Avatar` component - ([#29594](https://github.com/backstage/backstage/pull/29594))
- Add `Collapsible` component - ([#29617](https://github.com/backstage/backstage/pull/29617))
- Add `TextField` component instead of `Field` + `Input` - ([#29364](https://github.com/backstage/backstage/pull/29364))
- Add `TableCellProfile` - ([#29600](https://github.com/backstage/backstage/pull/29600))
- Add breakpoint hooks - `up()` and `down()` - ([#29564](https://github.com/backstage/backstage/pull/29564))
- Add gray scale css tokens - ([#29543](https://github.com/backstage/backstage/pull/29543))
- Update CSS styling API using `[data-___]` instead of class names for props - ([#29560](https://github.com/backstage/backstage/pull/29560))
- Update `Checkbox` dark mode - ([#29544](https://github.com/backstage/backstage/pull/29544))
- Update `Container` styles - ([#29475](https://github.com/backstage/backstage/pull/29475))
- Update `Menu` styles - ([#29351](https://github.com/backstage/backstage/pull/29351))
- Fix `Select` styles on small sizes + with long option names - ([#29545](https://github.com/backstage/backstage/pull/29545))
- Fix render prop on `Link` - ([#29247](https://github.com/backstage/backstage/pull/29247))
- Remove `Field` from `TextField` + `Select` - ([#29482](https://github.com/backstage/backstage/pull/29482))
- Update `textDecoration` to `none` on `Text` / `Heading` - ([#29357](https://github.com/backstage/backstage/pull/29357))
### Notable fixes
- Docs - Use stories from Storybook for all examples in Nextjs - ([#29306](https://github.com/backstage/backstage/pull/29306))
- Docs - Add release page (this one 🤗) - ([#29461](https://github.com/backstage/backstage/pull/29461))
- Docs - Add docs for Menu, Link - ([#29576](https://github.com/backstage/backstage/pull/29576))
- Fix CSS watch mode - ([#29352](https://github.com/backstage/backstage/pull/29352))
## Version 0.2.0
### Main updates
@@ -207,19 +207,19 @@ color of your app.
<Table.Cell>
<Chip head>--canon-bg-danger</Chip>
</Table.Cell>
<Table.Cell>Used to show errors informations.</Table.Cell>
<Table.Cell>Used to show errors information.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-bg-warning</Chip>
</Table.Cell>
<Table.Cell>Used to show warnings informations.</Table.Cell>
<Table.Cell>Used to show warnings information.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-bg-success</Chip>
</Table.Cell>
<Table.Cell>Used to show success informations.</Table.Cell>
<Table.Cell>Used to show success information.</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
@@ -39,7 +39,7 @@ Use the following advanced settings:
- `Session cookie name` = `AWSELBAuthSessionCookie`
- `Session timeout` = `604800` seconds
- `Scope` = `openid profile offline_access`
- `Action on unauthenticated request` = `Autenticate (client reattempt)`
- `Action on unauthenticated request` = `Authenticate (client reattempt)`
Once you've saved the action, you should see an authentication flow be triggered against Entra ID when visiting Backstage address at `https://backstage.yourdomain.com`. The flow will not complete successfully as the Backstage app isn't yet configured properly.
@@ -307,7 +307,7 @@ While a codemod for the New JSX Transform was originally introduced in the [Intr
j(path).replaceWith(j.jsxIdentifier(property));
});
// Add exisiting React imports to map
// Add existing React imports to map
reactImportPaths.forEach(path => {
const specifiers = path.value.specifiers;
for (let i = 0; i < specifiers.length; i++) {
-4
View File
@@ -66,10 +66,6 @@ function takes an `express.Response`, a `WebMessageResponse` and the URL of the
frontend (`appOrigin`) as parameters and return an HTML page with the script and
the message.
There is a helper class for [OAuth2](https://oauth.net/2/) based authentication providers, [OAuthAdapter](../reference/plugin-auth-backend.oauthadapter.md). This class implements the `AuthProviderRouteHandlers` interface
for you, and instead requires you to implement [OAuthHandlers](../reference/plugin-auth-backend.oauthhandlers.md), which
is significantly easier.
### Auth Environment Separation
The concept of an `env` is core to the way the auth backend works. It uses an
+1 -1
View File
@@ -22,7 +22,7 @@ be mapped to user identities within Backstage.
## Quick Start
> See [providers](../reference/plugin-auth-backend.providers.md)
> See [the auth docs](./index.md)
> for a full list of auth providers and their built-in sign-in resolvers.
Backstage projects created with `npx @backstage/create-app` come configured with a
@@ -237,7 +237,7 @@ The development server created above will be automatically configured with the d
```ts title="in dev/index.js"
//...
// This package should be installed as `devDependecies`
// This package should be installed as `devDependencies`
import { mockServices } from '@backstage/backend-test-utils';
const backend = createBackend();
+5 -9
View File
@@ -141,15 +141,7 @@ from `@backstage/core-plugin-api`.
## Accessing ConfigApi in Backend Plugins
### Old Backend System
In the old backend system plugins, the configuration is passed in via options from the main
backend package. See for example
[packages/backend-legacy/src/plugins/auth.ts](https://github.com/backstage/backstage/blob/244eef851f5aa19f91c7c9b5c12d5df95cf482ca/packages/backend/src/plugins/auth.ts#L23).
### New Backend System
In the new backend system, plugins are able to directly access config through dependencies. You can access config like so,
In the backend system, plugins are able to directly access config through dependencies. You can access config like so,
```ts title="plugins/your-plugin-backend/src/plugin.ts"
export const yourPlugin = createBackendPlugin({
@@ -175,3 +167,7 @@ export const yourPlugin = createBackendPlugin({
},
});
```
### Old Backend System
In the old backend system plugins, the configuration is passed in via options from the main backend package.
+2
View File
@@ -43,6 +43,8 @@ submitting them. You'll find the website sources under [/microsite](https://gith
with instructions for building and locally serving the website in the
[README](/microsite#readme).
For additional information and helpful guidelines on how to contribute to the documentation, check out these [Documentation Guidelines](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md#documentation-guidelines)!
### Contribute to Storybook
We think the best way to ensure different plugins provide a consistent experience is through a solid set of reusable UI/UX components. Backstage uses [Storybook](http://backstage.io/storybook).
@@ -13,7 +13,7 @@ it also defines what authentication metadata about a Kubernetes cluster is retur
## Context
Backstage includes by default some [Kubernetes Auth Providers](./authentication.md) to ease the authentication proccess to
Backstage includes by default some [Kubernetes Auth Providers](./authentication.md) to ease the authentication process to
kubernetes clusters, it includes:
- `Server Side Providers` like `localKubectlProxy` or `serviceAccount` where the same set
@@ -53,9 +53,7 @@ Some defining traits of entity providers:
### Creating an Entity Provider
The recommended way of instantiating the catalog backend classes is to use the
`CatalogBuilder`, as illustrated in the
[example backend here](https://github.com/backstage/backstage/blob/master/packages/backend-legacy/src/plugins/catalog.ts).
The recommended way of instantiating the catalog backend classes is to use the `CatalogBuilder`.
We will create a new
[`EntityProvider`](https://github.com/backstage/backstage/blob/master/plugins/catalog-node/src/api/provider.ts)
subclass that can be added to this catalog builder.
@@ -637,9 +635,7 @@ does so!
### Creating a Catalog Data Reader Processor
The recommended way of instantiating the catalog backend classes is to use the
`CatalogBuilder`, as illustrated in the
[example backend here](https://github.com/backstage/backstage/blob/master/packages/backend-legacy/src/plugins/catalog.ts).
The recommended way of instantiating the catalog backend classes is to use the `CatalogBuilder`.
We will create a new
[`CatalogProcessor`](https://github.com/backstage/backstage/blob/master/plugins/catalog-node/src/api/processor.ts)
subclass that can be added to this catalog builder.
+1 -1
View File
@@ -29,7 +29,7 @@ On the user experience side, a Backstage experience without complete organizatio
While it's possible to get hold of a catalog client via the `catalogServiceRef` from `@backstage/plugin-catalog-node`, it's almost never the right thing to do, and we strongly discourage from doing so.
The catalog processing loop is a very high-speed system where your entire catalog cluster collaborates to race through all entities at the highest possible rate. The ideal processor does an absolute minimum of work, and immediately relinquishes control back. Performing asynchronous requests to external systems - including the catalog - from processors, can quickly become overwhelming for that external system and starve their resources if they aren't prepared to deal with very high rates of small requests. It also significantly slows down the procesing loop, when each step needs to wait for responses. This can lead to work "piling up" in the catalog and delays in seeing entities get updated. The [life of an entity](./life-of-an-entity.md) article shows the sequence of events that happen when an entity goes from original ingestion, through processing, and to becoming final entities.
The catalog processing loop is a very high-speed system where your entire catalog cluster collaborates to race through all entities at the highest possible rate. The ideal processor does an absolute minimum of work, and immediately relinquishes control back. Performing asynchronous requests to external systems - including the catalog - from processors, can quickly become overwhelming for that external system and starve their resources if they aren't prepared to deal with very high rates of small requests. It also significantly slows down the processing loop, when each step needs to wait for responses. This can lead to work "piling up" in the catalog and delays in seeing entities get updated. The [life of an entity](./life-of-an-entity.md) article shows the sequence of events that happen when an entity goes from original ingestion, through processing, and to becoming final entities.
See also [the related validation topic](#can-i-validate-relations-in-processors).
@@ -118,7 +118,7 @@ export const mockDecorator = createScaffolderFormDecorator({
// give the decorator a name
id: 'mock-decorator',
// define the schema for the input that can be proided in `template.yaml`
// define the schema for the input that can be provided in `template.yaml`
schema: {
input: {
test: z => z.string(),
@@ -151,7 +151,7 @@ const routes = (
### Async Validation Function
A validation function can be asyncronous and use [Utility APIs](https://backstage.io/docs/api/utility-apis/) via the `ApiHolder` in the [field validation context](https://backstage.io/docs/reference/plugin-scaffolder-react.customfieldvalidator). The example below uses the `catalogApiRef` to check if the submitted value (in this scenario an entity ref) exists in the catalog.
A validation function can be asynchronous and use [Utility APIs](https://backstage.io/docs/api/utility-apis/) via the `ApiHolder` in the [field validation context](https://backstage.io/docs/reference/plugin-scaffolder-react.customfieldvalidator). The example below uses the `catalogApiRef` to check if the submitted value (in this scenario an entity ref) exists in the catalog.
```tsx
import { FieldValidation } from '@rjsf/utils';
@@ -252,7 +252,7 @@ spec:
type: service
parameters:
- title: Authenticaion
- title: Authentication
description: Provide authentication for the resource
required:
- username
+1 -1
View File
@@ -934,7 +934,7 @@ metadata:
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: example-platfrom
name: example-platform
title: Example Application Platform
namespace: default
description: This is the child entity
@@ -93,7 +93,7 @@ The extension ID of the work API will be the kind `api:` followed by the plugin
## Adding configurability
Here we will describe how to amend a utility API with the capability of having extension config, which is driven by [your app-config](../../conf/writing.md). You do this by giving an extension config schema to your API extension factory function. Let's refactory the example above to also accept configuration, which will require us to use the [override method of the blueprint](../architecture/23-extension-blueprints.md#creating-an-extension-from-a-blueprint-with-overrides).
Here we will describe how to amend a utility API with the capability of having extension config, which is driven by [your app-config](../../conf/writing.md). You do this by giving an extension config schema to your API extension factory function. Let's refactor the example above to also accept configuration, which will require us to use the [override method of the blueprint](../architecture/23-extension-blueprints.md#creating-an-extension-from-a-blueprint-with-overrides).
```tsx title="in @internal/plugin-example"
const exampleWorkApi = ApiBlueprint.makeWithOverrides({
@@ -123,7 +123,7 @@ We wanted users to be able to set a `goSlow` extension config parameter for our
Note that the expression "extension config" as used here, is _not_ the same thing as the `configApi` which gives you access to the full app-config. The extension config discussed here is instead the particular configuration settings given to your utility API instance. This is discussed more [in the Configuring section](./04-configuring.md).
Note also that the extension config schema contained a default value fo the `goSlow` field. This is an important consideration. You want users of your API to be able to get maximum value out of it, without having to dive deep into how to configure it. For that reason you generally want to provide as many sane defaults as possible, while letting users override them rarely but with purpose, only when called for. If you have an extension config schema without defaults, the framework will refuse to instantiate the utility API on startup unless the user had configured those values explicitly. Since it had a default value, the TypeScript code and interfaces also don't have to defensively allow `undefined` - we know that it'll have either the default value or an overridden value when we start consuming the extension config data.
Note also that the extension config schema contained a default value for the `goSlow` field. This is an important consideration. You want users of your API to be able to get maximum value out of it, without having to dive deep into how to configure it. For that reason you generally want to provide as many sane defaults as possible, while letting users override them rarely but with purpose, only when called for. If you have an extension config schema without defaults, the framework will refuse to instantiate the utility API on startup unless the user had configured those values explicitly. Since it had a default value, the TypeScript code and interfaces also don't have to defensively allow `undefined` - we know that it'll have either the default value or an overridden value when we start consuming the extension config data.
## Adding inputs
+1 -1
View File
@@ -47,7 +47,7 @@ Additional steps for the main line release
- Check for mentions of "major" & "breaking" and if they are expected in the current release
- Verify the version we are shipping is correct
- Create Release Notes
- There exists a [release notes template](./release-notes-template.md) for creating the release notes. It can already be created after the last main line release to keep track of major changes during the month
- There exists a [release notes template](./.release-notes-template.md) for creating the release notes. It can already be created after the last main line release to keep track of major changes during the month
- The content is picked by relevancy showcasing the work of the community during the month of the release
- Mention newly added packages or features
- Mention any security fixes
+1 -1
View File
@@ -51,7 +51,7 @@ This means that the translation keys have changed for `actionsPage.content.table
Contributed by [@mbenson](https://github.com/mbenson) in [#29383](https://github.com/backstage/backstage/pull/29383)
### `backtage-cli repo start`
### `backstage-cli repo start`
In order to align on `yarn start` being the only command needed for local development, weve introduced a new `repo start` command to the `backstage-cli` for use in the root `package.json`.
@@ -25,7 +25,7 @@ For example:
- Should the POC be deployed on Kubernetes?
- How do you deploy the POC on my cloud provider?
To address these questions and reduce friction from deploying a POC, we built a [new CLI](https://github.com/backstage/backstage-deploy). The CLI is simply called `deploy` and is invokable with `npx`. With this new CLI, you can generate a Dockerfile and deploy a Backstage instance onto a preferred cloud provider. While the packages infrastructure is built to support all cloud providers, currently the CLI only offers an AWS implementation. In the future, we plan to add additional cloud providers to the package and [welcome any contributions](https://github.com/backstage/backstage/blob/6996b3338d678efc03307112524060e9dc2ad769/CONTRIBUTING.md) extending the suite of cloud provider implementations!
To address these questions and reduce friction from deploying a POC, we built a [new CLI](https://github.com/backstage/backstage-deploy). The CLI is simply called `deploy` and is invocable with `npx`. With this new CLI, you can generate a Dockerfile and deploy a Backstage instance onto a preferred cloud provider. While the packages infrastructure is built to support all cloud providers, currently the CLI only offers an AWS implementation. In the future, we plan to add additional cloud providers to the package and [welcome any contributions](https://github.com/backstage/backstage/blob/6996b3338d678efc03307112524060e9dc2ad769/CONTRIBUTING.md) extending the suite of cloud provider implementations!
**So, what about Kubernetes?** Since the CLI is designed specifically for the POC phase, we believe that Kubernetes isnt the right fit as Kubernetes is better suited for production workloads. So, we explored lightweight, container-based solutions, and landed on [Amazon Lightsail](https://docs.aws.amazon.com/lightsail/index.html) as a hosting service for the POC.
Amazon Lightsail supports [lightweight container](https://lightsail.aws.amazon.com/ls/docs/en_us/articles/amazon-lightsail-container-services) deployments [at a low cost](https://aws.amazon.com/lightsail/pricing/), and has a [free trial for new users](https://aws.amazon.com/lightsail/pricing/?loc=ft#AWS_Free_Tier)! Given Lightsails ease of use and affordability, we recommend using Lightsail over Kubernetes to test out your Backstage POC.
@@ -74,7 +74,7 @@ While Kubernetes observability was requested to be provided out of the box, we q
- Fetching runtime security vulnerability information across different stages
![Security vulnerabilities in context](assets/2024-09-24/backstage_catalog_security_vulnerabilites.png)
![Security vulnerabilities in context](assets/2024-09-24/backstage_catalog_security_vulnerabilities.png)
### Error logs at hand
+1 -1
View File
@@ -3,7 +3,7 @@ title: Backchat GenAI
author: benwilcock
authorUrl: https://github.com/benwilcock
category: Services
description: Access your favorite open source GenAI GUIs privately from Backstage. Chat wth large language models in your portal. Choose from hundreds of LLMs. Run inferencing wherever you like - local or remote, CPU or GPU - it's up to you!
description: Access your favorite open source GenAI GUIs privately from Backstage. Chat with large language models in your portal. Choose from hundreds of LLMs. Run inferencing wherever you like - local or remote, CPU or GPU - it's up to you!
documentation: https://github.com/benwilcock/backstage-plugin-backchat
iconUrl: /img/backchat-logo.png
npmPackageName: '@benbravo73/backstage-plugin-backchat'
+1 -1
View File
@@ -8,6 +8,6 @@ documentation: https://github.com/harness/backstage-plugins/tree/main/plugins/ha
iconUrl: https://static.harness.io/ng-static/images/favicon.png
npmPackageName: '@harnessio/backstage-plugin-harness-iacm'
tags:
- infrastrucure-as-code
- infrastructure-as-code
- resource-management
addedDate: '2024-07-03'
+1 -1
View File
@@ -3,7 +3,7 @@ title: Statuspage.io Plugin
author: AxisCommunications
authorUrl: https://github.com/AxisCommunications
category: Monitoring
description: The Statuspage plugin allows you to embedd https://statuspage.io components, component groups and dashboards in Backstage.
description: The Statuspage plugin allows you to embed https://statuspage.io components, component groups and dashboards in Backstage.
documentation: https://github.com/AxisCommunications/backstage-plugins/blob/main/plugins/statuspage/README.md
iconUrl: https://raw.githubusercontent.com/AxisCommunications/backstage-plugins/main/plugins/statuspage/media/logo.png
npmPackageName: '@axis-backstage/plugin-statuspage'
+3 -1
View File
@@ -165,7 +165,9 @@ const Community = () => {
<BannerSectionGrid
header={
<>
<h2 className="text--primary">Offical Backstage Initiatives</h2>
<h2 className="text--primary">
Official Backstage Initiatives
</h2>
<h1>Stay tuned to the latest developments</h1>
</>
-1
View File
@@ -52,7 +52,6 @@
"snyk:test:package": "yarn snyk:test --include",
"start": "backstage-cli repo start",
"start-backend": "echo \"Use 'yarn start example-backend' instead\"",
"start-backend:legacy": "echo \"Use 'yarn start example-backend-legacy' instead\"",
"start:microsite": "cd microsite/ && yarn start",
"start:next": "yarn start example-app-next example-backend",
"storybook": "yarn ./storybook run storybook",
-1
View File
@@ -1 +0,0 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
-10
View File
@@ -1,10 +0,0 @@
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.22.1
# ignores vulnerabilities until expiry date; change duration by modifying expiry date
ignore:
SNYK-JS-ISOLATEDVM-3037320:
- '*':
reason: We do not pass any V8 cache data, and are therefore unaffected by this vulnerability
expires: 2033-07-02T16:55:57.077Z
created: 2023-07-02T16:55:57.077Z
patch: {}
File diff suppressed because it is too large Load Diff
-61
View File
@@ -1,61 +0,0 @@
# example-backend-legacy
This package is an EXAMPLE of a Backstage backend using the old backend system.
The main purpose of this package is to provide a test bed for Backstage plugins
that have a backend part. Feel free to experiment locally or within your fork
by adding dependencies and routes to this backend, to try things out.
By running the `@backstage/create-app` script, you get your own separate Backstage backend.
## Development
To run the example backend, first go to the project root and run
```bash
yarn install
```
You should only need to do this once.
After that, go to the `packages/backend-legacy` directory and run
```bash
yarn start
```
If you want to override any configuration locally, for example adding any secrets,
you can do so in `app-config.local.yaml`.
The backend starts up on port 7007 per default.
### Debugging
The backend is a node process that can be inspected to allow breakpoints and live debugging. To enable this, pass the `--inspect` flag to [backend:dev](https://backstage.io/docs/tooling/cli/build-system#backend-development).
To debug the backend in [Visual Studio Code](https://code.visualstudio.com/):
- Enable Auto Attach (⌘ + Shift + P > Toggle Auto Attach > Only With Flag)
- Open a VSCode terminal (Control + `)
- Run the backend from the VSCode terminal: `yarn start-backend:legacy --inspect`
## Populating The Catalog
If you want to use the catalog functionality, you need to add so called
locations to the backend. These are places where the backend can find some
entity descriptor data to consume and serve. For more information, see
[Software Catalog Overview - Adding Components to the Catalog](https://backstage.io/docs/features/software-catalog/#adding-components-to-the-catalog).
For convenience we already include some statically configured example locations
in `app-config.yaml` under `catalog.locations`. For local development you can override these in your own `app-config.local.yaml`.
## Authentication
We chose [Passport](http://www.passportjs.org/) as authentication platform due to its comprehensive set of supported authentication [strategies](http://www.passportjs.org/packages/).
Read more about the [auth-backend](https://github.com/backstage/backstage/blob/master/plugins/auth-backend/README.md) and [how to add a new provider](https://github.com/backstage/backstage/blob/master/docs/auth/add-auth-provider.md)
## Documentation
- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md)
- [Backstage Documentation](https://backstage.io/docs)
@@ -1,9 +0,0 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: example-backend-legacy
title: example-backend-legacy
spec:
lifecycle: experimental
type: backstage-backend
owner: maintainers
-27
View File
@@ -1,27 +0,0 @@
# Knip report
## Unused dependencies (12)
| Name | Location | Severity |
| :------------------------------------------------- | :----------- | :------- |
| @backstage/plugin-scaffolder-backend-module-gitlab | package.json | error |
| @backstage/plugin-scaffolder-backend-module-rails | package.json | error |
| @backstage/plugin-search-backend-module-catalog | package.json | error |
| @backstage/plugin-signals-backend | package.json | error |
| azure-devops-node-api | package.json | error |
| @gitbeaker/node | package.json | error |
| better-sqlite3 | package.json | error |
| @octokit/rest | package.json | error |
| dockerode | package.json | error |
| mysql2 | package.json | error |
| luxon | package.json | error |
| pg | package.json | error |
## Unused devDependencies (3)
| Name | Location | Severity |
| :------------------------------- | :----------- | :------- |
| @types/express-serve-static-core | package.json | error |
| @types/dockerode | package.json | error |
| @types/luxon | package.json | error |
-86
View File
@@ -1,86 +0,0 @@
{
"name": "example-backend-legacy",
"version": "0.2.109",
"backstage": {
"role": "backend"
},
"private": true,
"keywords": [
"backstage"
],
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "packages/backend-legacy"
},
"license": "Apache-2.0",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"files": [
"dist"
],
"scripts": {
"build": "backstage-cli package build",
"clean": "backstage-cli package clean",
"lint": "backstage-cli package lint",
"start": "backstage-cli package start",
"test": "backstage-cli package test"
},
"dependencies": {
"@backstage/backend-common": "^0.25.0",
"@backstage/backend-defaults": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/catalog-client": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/integration": "workspace:^",
"@backstage/plugin-auth-backend": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"@backstage/plugin-catalog-backend": "workspace:^",
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "workspace:^",
"@backstage/plugin-catalog-backend-module-unprocessed": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^",
"@backstage/plugin-events-backend": "workspace:^",
"@backstage/plugin-events-node": "workspace:^",
"@backstage/plugin-kubernetes-backend": "workspace:^",
"@backstage/plugin-permission-backend": "workspace:^",
"@backstage/plugin-permission-common": "workspace:^",
"@backstage/plugin-permission-node": "workspace:^",
"@backstage/plugin-scaffolder-backend": "workspace:^",
"@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "workspace:^",
"@backstage/plugin-scaffolder-backend-module-gitlab": "workspace:^",
"@backstage/plugin-scaffolder-backend-module-rails": "workspace:^",
"@backstage/plugin-search-backend": "workspace:^",
"@backstage/plugin-search-backend-module-catalog": "workspace:^",
"@backstage/plugin-search-backend-module-elasticsearch": "workspace:^",
"@backstage/plugin-search-backend-module-explore": "workspace:^",
"@backstage/plugin-search-backend-module-pg": "workspace:^",
"@backstage/plugin-search-backend-module-techdocs": "workspace:^",
"@backstage/plugin-search-backend-node": "workspace:^",
"@backstage/plugin-signals-backend": "workspace:^",
"@backstage/plugin-signals-node": "workspace:^",
"@backstage/plugin-techdocs-backend": "workspace:^",
"@gitbeaker/node": "^35.1.0",
"@octokit/rest": "^19.0.3",
"azure-devops-node-api": "^14.0.0",
"better-sqlite3": "^11.0.0",
"dockerode": "^4.0.0",
"express": "^4.17.1",
"express-prom-bundle": "^7.0.0",
"express-promise-router": "^4.1.0",
"luxon": "^3.0.0",
"mysql2": "^3.0.0",
"pg": "^8.11.3",
"pg-connection-string": "^2.3.0",
"prom-client": "^15.0.0",
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@types/dockerode": "^3.3.0",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5",
"@types/luxon": "^3.0.0"
}
}
-24
View File
@@ -1,24 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { PluginEnvironment } from './types';
describe('test', () => {
it('unbreaks the test runner', () => {
const unbreaker = {} as PluginEnvironment;
expect(unbreaker).toBeTruthy();
});
});
-158
View File
@@ -1,158 +0,0 @@
/*
* Copyright 2022 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.
*/
/*
* Hi!
*
* Note that this is an EXAMPLE Backstage backend. Please check the README.
*
* Happy hacking!
*/
import Router from 'express-promise-router';
import {
CacheManager,
createLegacyAuthAdapters,
createServiceBuilder,
DatabaseManager,
getRootLogger,
HostDiscovery,
loadBackendConfig,
notFoundHandler,
ServerTokenManager,
useHotMemoize,
} from '@backstage/backend-common';
import { Config } from '@backstage/config';
import healthcheck from './plugins/healthcheck';
import { metricsHandler, metricsInit } from './metrics';
import authPlugin from './plugins/auth';
import catalog from './plugins/catalog';
import events from './plugins/events';
import kubernetes from './plugins/kubernetes';
import scaffolder from './plugins/scaffolder';
import permission from './plugins/permission';
import { PluginEnvironment } from './types';
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
import { DefaultEventsService } from '@backstage/plugin-events-node';
import { DefaultSignalsService } from '@backstage/plugin-signals-node';
import { UrlReaders } from '@backstage/backend-defaults/urlReader';
import { DefaultSchedulerService } from '@backstage/backend-defaults/scheduler';
function makeCreateEnv(config: Config) {
const root = getRootLogger();
const reader = UrlReaders.default({ logger: root, config });
const discovery = HostDiscovery.fromConfig(config);
const tokenManager = ServerTokenManager.fromConfig(config, { logger: root });
const { auth } = createLegacyAuthAdapters({
auth: undefined,
discovery,
tokenManager,
});
const permissions = ServerPermissionClient.fromConfig(config, {
discovery,
auth,
});
const databaseManager = DatabaseManager.fromConfig(config, { logger: root });
const cacheManager = CacheManager.fromConfig(config);
const identity = DefaultIdentityClient.create({
discovery,
});
const eventsService = DefaultEventsService.create({ logger: root, config });
const signalsService = DefaultSignalsService.create({
events: eventsService,
});
root.info(`Created UrlReader ${reader}`);
return (plugin: string): PluginEnvironment => {
const logger = root.child({ type: 'plugin', plugin });
const database = databaseManager.forPlugin(plugin);
const cache = cacheManager.forPlugin(plugin);
const scheduler = DefaultSchedulerService.create({
logger,
database,
});
return {
logger,
cache,
database,
config,
reader,
events: eventsService,
discovery,
tokenManager,
permissions,
scheduler,
identity,
signals: signalsService,
};
};
}
async function main() {
metricsInit();
const logger = getRootLogger();
logger.info(
`You are running an example backend, which is supposed to be mainly used for contributing back to Backstage. ` +
`Do NOT deploy this to production. Read more here https://backstage.io/docs/getting-started/`,
);
const config = await loadBackendConfig({
argv: process.argv,
logger,
});
const createEnv = makeCreateEnv(config);
const healthcheckEnv = useHotMemoize(module, () => createEnv('healthcheck'));
const catalogEnv = useHotMemoize(module, () => createEnv('catalog'));
const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
const authEnv = useHotMemoize(module, () => createEnv('auth'));
const kubernetesEnv = useHotMemoize(module, () => createEnv('kubernetes'));
const permissionEnv = useHotMemoize(module, () => createEnv('permission'));
const eventsEnv = useHotMemoize(module, () => createEnv('events'));
const apiRouter = Router();
apiRouter.use('/catalog', await catalog(catalogEnv));
apiRouter.use('/events', await events(eventsEnv));
apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv));
apiRouter.use('/auth', await authPlugin(authEnv));
apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv));
apiRouter.use('/permission', await permission(permissionEnv));
apiRouter.use(notFoundHandler());
const service = createServiceBuilder(module)
.loadConfig(config)
.addRouter('', await healthcheck(healthcheckEnv))
.addRouter('', metricsHandler())
.addRouter('/api', apiRouter);
await service.start().catch(err => {
logger.error(err);
process.exit(1);
});
}
module.hot?.accept();
main().catch(error => {
console.error('Backend failed to start up', error);
process.exit(1);
});
-63
View File
@@ -1,63 +0,0 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useHotCleanup } from '@backstage/backend-common';
import { RequestHandler, Request } from 'express';
import promBundle from 'express-prom-bundle';
import prom from 'prom-client';
import * as url from 'url';
/**
* Experimental Prometheus metrics used to benchmark the performance of the
* software catalog. Use this at your own risk.
*/
const rootRegEx = new RegExp('^/([^/]*)/.*');
const apiRegEx = new RegExp('^/api/([^/]*)/.*');
function normalizePath(req: Request): string {
const path = url.parse(req.originalUrl || req.url).pathname || '/';
// Capture /api/ and the plugin name
if (apiRegEx.test(path)) {
return path.replace(apiRegEx, '/api/$1');
}
// Only the first path segment at root level
return path.replace(rootRegEx, '/$1');
}
export function metricsInit(): void {
prom.collectDefaultMetrics({ prefix: 'backstage_' });
}
/**
* Adds a /metrics endpoint, register default runtime metrics and instrument the router.
*/
export function metricsHandler(): RequestHandler {
// We can only initialize the metrics once and have to clean them up between hot reloads
useHotCleanup(module, () => prom.register.clear());
return promBundle({
includeMethod: true,
includePath: true,
// Using includePath alone is problematic, as it will include path labels with high
// cardinality (e.g. path params). Instead we would have to template them. However, this
// is difficult, as every backend plugin might use different routes. Instead we only take
// the first directory of the path, to have at least an idea how each plugin performs:
normalizePath,
promClient: { collectDefaultMetrics: {} },
});
}
@@ -1,60 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
EntityProvider,
EntityProviderConnection,
} from '@backstage/plugin-catalog-node';
import { EventParams, EventsService } from '@backstage/plugin-events-node';
import { Logger } from 'winston';
export class DemoEventBasedEntityProvider implements EntityProvider {
private readonly logger: Logger;
private readonly events: EventsService;
private readonly topics: string[];
constructor(opts: {
events: EventsService;
logger: Logger;
topics: string[];
}) {
this.events = opts.events;
this.logger = opts.logger;
this.topics = opts.topics;
}
async subscribe() {
await this.events.subscribe({
id: 'DemoEventBasedEntityProvider',
topics: this.topics,
onEvent: async (params: EventParams): Promise<void> => {
this.logger.info(
`onEvent: topic=${params.topic}, metadata=${JSON.stringify(
params.metadata,
)}, payload=${JSON.stringify(params.eventPayload)}`,
);
},
});
}
async connect(_: EntityProviderConnection): Promise<void> {
// not doing anything here
}
getProviderName(): string {
return DemoEventBasedEntityProvider.name;
}
}
-146
View File
@@ -1,146 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
DEFAULT_NAMESPACE,
stringifyEntityRef,
} from '@backstage/catalog-model';
import {
createRouter,
providers,
defaultAuthProviderFactories,
} from '@backstage/plugin-auth-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
return await createRouter({
logger: env.logger,
config: env.config,
database: env.database,
discovery: env.discovery,
tokenManager: env.tokenManager,
providerFactories: {
...defaultAuthProviderFactories,
// NOTE: DO NOT add this many resolvers in your own instance!
// It is important that each real user always gets resolved to
// the same sign-in identity. The code below will not do that.
// It is here for demo purposes only.
github: providers.github.create({
signIn: {
async resolver({ result: { fullProfile } }, ctx) {
const userId = fullProfile.username;
if (!userId) {
throw new Error(
`GitHub user profile does not contain a username`,
);
}
const userEntityRef = stringifyEntityRef({
kind: 'User',
name: userId,
namespace: DEFAULT_NAMESPACE,
});
return ctx.issueToken({
claims: {
sub: userEntityRef,
ent: [userEntityRef],
},
});
},
},
}),
gitlab: providers.gitlab.create({
signIn: {
async resolver({ result: { fullProfile } }, ctx) {
return ctx.signInWithCatalogUser({
entityRef: {
name: fullProfile.id,
},
});
},
},
}),
microsoft: providers.microsoft.create({
signIn: {
resolver:
providers.microsoft.resolvers.emailMatchingUserEntityAnnotation(),
},
}),
google: providers.google.create({
signIn: {
resolver:
providers.google.resolvers.emailLocalPartMatchingUserEntityName(),
},
}),
okta: providers.okta.create({
signIn: {
resolver:
providers.okta.resolvers.emailMatchingUserEntityAnnotation(),
},
}),
bitbucket: providers.bitbucket.create({
signIn: {
resolver:
providers.bitbucket.resolvers.usernameMatchingUserEntityAnnotation(),
},
}),
onelogin: providers.onelogin.create({
signIn: {
async resolver({ result: { fullProfile } }, ctx) {
return ctx.signInWithCatalogUser({
entityRef: {
name: fullProfile.id,
},
});
},
},
}),
bitbucketServer: providers.bitbucketServer.create({
signIn: {
resolver:
providers.bitbucketServer.resolvers.emailMatchingUserEntityProfileEmail(),
},
}),
// This is an example of how to configure the OAuth2Proxy provider as well
// as how to sign a user in without a matching user entity in the catalog.
// You can try it out using `<ProxiedSignInPage {...props} provider="myproxy" />`
myproxy: providers.oauth2Proxy.create({
signIn: {
async resolver({ result }, ctx) {
const entityRef = stringifyEntityRef({
kind: 'user',
namespace: DEFAULT_NAMESPACE,
name: result.getHeader('x-forwarded-user')!,
});
return ctx.issueToken({
claims: {
sub: entityRef,
ent: [entityRef],
},
});
},
},
}),
},
});
}
@@ -1,51 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CatalogBuilder } from '@backstage/plugin-catalog-backend';
import { ScaffolderEntitiesProcessor } from '@backstage/plugin-catalog-backend-module-scaffolder-entity-model';
import { UnprocessedEntitiesModule } from '@backstage/plugin-catalog-backend-module-unprocessed';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
import { DemoEventBasedEntityProvider } from './DemoEventBasedEntityProvider';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const builder = CatalogBuilder.create(env);
builder.addProcessor(new ScaffolderEntitiesProcessor());
const demoProvider = new DemoEventBasedEntityProvider({
events: env.events,
logger: env.logger,
topics: ['example'],
});
await demoProvider.subscribe();
builder.addEntityProvider(demoProvider);
const { processingEngine, router } = await builder.build();
const unprocessed = UnprocessedEntitiesModule.create({
database: await env.database.getClient(),
router,
permissions: env.permissions,
discovery: env.discovery,
});
unprocessed.registerRoutes();
await processingEngine.start();
return router;
}
@@ -1,34 +0,0 @@
/*
* Copyright 2022 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 { HttpPostIngressEventPublisher } from '@backstage/plugin-events-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const eventsRouter = Router();
const http = HttpPostIngressEventPublisher.fromConfig({
config: env.config,
events: env.events,
logger: env.logger,
});
http.bind(eventsRouter);
return eventsRouter;
}
@@ -1,28 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createStatusCheckRouter } from '@backstage/backend-common';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
return await createStatusCheckRouter({
logger: env.logger,
path: '/healthcheck',
});
}
@@ -1,34 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { KubernetesBuilder } from '@backstage/plugin-kubernetes-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
import { CatalogClient } from '@backstage/catalog-client';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const catalogApi = new CatalogClient({ discoveryApi: env.discovery });
const { router } = await KubernetesBuilder.createBuilder({
logger: env.logger,
config: env.config,
catalogApi,
permissions: env.permissions,
discovery: env.discovery,
}).build();
return router;
}
@@ -1,53 +0,0 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { BackstageIdentityResponse } from '@backstage/plugin-auth-node';
import { createRouter } from '@backstage/plugin-permission-backend';
import {
AuthorizeResult,
PolicyDecision,
} from '@backstage/plugin-permission-common';
import {
PermissionPolicy,
PolicyQuery,
} from '@backstage/plugin-permission-node';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
class ExamplePermissionPolicy implements PermissionPolicy {
async handle(
_request: PolicyQuery,
_user?: BackstageIdentityResponse,
): Promise<PolicyDecision> {
// some logic to determine if the user is allowed to access the resource
return {
result: AuthorizeResult.ALLOW,
};
}
}
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
return await createRouter({
config: env.config,
logger: env.logger,
discovery: env.discovery,
policy: new ExamplePermissionPolicy(),
identity: env.identity,
});
}
@@ -1,63 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CatalogClient } from '@backstage/catalog-client';
import {
createBuiltinActions,
createRouter,
} from '@backstage/plugin-scaffolder-backend';
import { Router } from 'express';
import type { PluginEnvironment } from '../types';
import { ScmIntegrations } from '@backstage/integration';
import { createConfluenceToMarkdownAction } from '@backstage/plugin-scaffolder-backend-module-confluence-to-markdown';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const catalogClient = new CatalogClient({
discoveryApi: env.discovery,
});
const integrations = ScmIntegrations.fromConfig(env.config);
const builtInActions = createBuiltinActions({
integrations,
config: env.config,
catalogClient,
reader: env.reader,
});
const actions = [
...builtInActions,
createConfluenceToMarkdownAction({
integrations,
config: env.config,
reader: env.reader,
}),
];
return await createRouter({
logger: env.logger,
config: env.config,
database: env.database,
catalogClient: catalogClient,
reader: env.reader,
discovery: env.discovery,
scheduler: env.scheduler,
permissions: env.permissions,
actions,
});
}
-44
View File
@@ -1,44 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Logger } from 'winston';
import { Config } from '@backstage/config';
import { PluginCacheManager, TokenManager } from '@backstage/backend-common';
import { IdentityApi } from '@backstage/plugin-auth-node';
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
import { EventsService } from '@backstage/plugin-events-node';
import { SignalsService } from '@backstage/plugin-signals-node';
import {
UrlReaderService,
SchedulerService,
DatabaseService,
DiscoveryService,
} from '@backstage/backend-plugin-api';
export type PluginEnvironment = {
logger: Logger;
cache: PluginCacheManager;
database: DatabaseService;
config: Config;
reader: UrlReaderService;
discovery: DiscoveryService;
tokenManager: TokenManager;
permissions: PermissionEvaluator;
scheduler: SchedulerService;
identity: IdentityApi;
events: EventsService;
signals: SignalsService;
};
+12 -12
View File
@@ -41,26 +41,26 @@
"test": "backstage-cli package test"
},
"dependencies": {
"@base-ui-components/react": "^1.0.0-alpha.5",
"@remixicon/react": "^4.5.0",
"@base-ui-components/react": "^1.0.0-alpha.7",
"@remixicon/react": "^4.6.0",
"@tanstack/react-table": "^8.21.3",
"clsx": "^2.1.1"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@storybook/addon-essentials": "^8.6.8",
"@storybook/addon-interactions": "^8.6.8",
"@storybook/addon-essentials": "^8.6.12",
"@storybook/addon-interactions": "^8.6.12",
"@storybook/addon-styling-webpack": "^1.0.1",
"@storybook/addon-themes": "^8.6.8",
"@storybook/addon-themes": "^8.6.12",
"@storybook/addon-webpack5-compiler-swc": "^3.0.0",
"@storybook/blocks": "^8.6.8",
"@storybook/react": "^8.6.8",
"@storybook/react-webpack5": "^8.6.8",
"@storybook/test": "^8.6.8",
"@tanstack/react-table": "^8.21.2",
"@storybook/blocks": "^8.6.12",
"@storybook/react": "^8.6.12",
"@storybook/react-webpack5": "^8.6.12",
"@storybook/test": "^8.6.12",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"chalk": "^5.4.1",
"eslint-plugin-storybook": "^0.11.4",
"eslint-plugin-storybook": "^0.12.0",
"glob": "^11.0.1",
"globals": "^15.11.0",
"lightningcss": "^1.29.1",
@@ -68,7 +68,7 @@
"react": "^18.0.2",
"react-dom": "^18.0.2",
"react-router-dom": "^6.3.0",
"storybook": "^8.6.8"
"storybook": "^8.6.12"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
@@ -16,6 +16,7 @@ import { GetEntityAncestorsRequest } from '@backstage/catalog-client';
import { GetEntityAncestorsResponse } from '@backstage/catalog-client';
import { GetEntityFacetsRequest } from '@backstage/catalog-client';
import { GetEntityFacetsResponse } from '@backstage/catalog-client';
import { GetLocationsResponse } from '@backstage/catalog-client';
import { Location as Location_2 } from '@backstage/catalog-client';
import { QueryEntitiesRequest } from '@backstage/catalog-client';
import { QueryEntitiesResponse } from '@backstage/catalog-client';
@@ -53,6 +54,8 @@ export class InMemoryCatalogClient implements CatalogApi {
// (undocumented)
getLocationByRef(_locationRef: string): Promise<Location_2 | undefined>;
// (undocumented)
getLocations(_request?: {}): Promise<GetLocationsResponse>;
// (undocumented)
queryEntities(request?: QueryEntitiesRequest): Promise<QueryEntitiesResponse>;
// (undocumented)
refreshEntity(_entityRef: string): Promise<void>;
+14
View File
@@ -62,6 +62,10 @@ export interface CatalogApi {
locationRef: string,
options?: CatalogRequestOptions,
): Promise<Location_2 | undefined>;
getLocations(
request?: {},
options?: CatalogRequestOptions,
): Promise<GetLocationsResponse>;
queryEntities(
request?: QueryEntitiesRequest,
options?: CatalogRequestOptions,
@@ -136,6 +140,10 @@ export class CatalogClient implements CatalogApi {
locationRef: string,
options?: CatalogRequestOptions,
): Promise<Location_2 | undefined>;
getLocations(
request?: {},
options?: CatalogRequestOptions,
): Promise<GetLocationsResponse>;
queryEntities(
request?: QueryEntitiesRequest,
options?: CatalogRequestOptions,
@@ -250,6 +258,12 @@ export interface GetEntityFacetsResponse {
>;
}
// @public
export interface GetLocationsResponse {
// (undocumented)
items: Location_2[];
}
// @public
type Location_2 = {
id: string;
@@ -24,6 +24,7 @@ import {
QueryEntitiesResponse,
} from './types/api';
import { DiscoveryApi } from './types/discovery';
import { GetLocations200ResponseInner } from './schema/openapi';
const server = setupServer();
const token = 'fake-token';
@@ -593,6 +594,88 @@ describe('CatalogClient', () => {
});
});
describe('getLocations', () => {
const defaultResponse = [
{
data: {
id: '42',
type: 'url',
target: 'https://example.com',
},
},
{
data: {
id: '43',
type: 'url',
target: 'https://example.com',
},
},
] satisfies GetLocations200ResponseInner[];
beforeEach(() => {
server.use(
rest.get(`${mockBaseUrl}/locations`, (_, res, ctx) => {
return res(ctx.json(defaultResponse));
}),
);
});
it('should return locations from correct endpoint', async () => {
const response = await client.getLocations({}, { token });
expect(response).toEqual({
items: [
{
id: '42',
type: 'url',
target: 'https://example.com',
},
{
id: '43',
type: 'url',
target: 'https://example.com',
},
],
});
});
it('should return empty list with empty result', async () => {
server.use(
rest.get(`${mockBaseUrl}/locations`, (_, res, ctx) => {
return res(ctx.json([]));
}),
);
const response = await client.getLocations({}, { token });
expect(response).toEqual({ items: [] });
});
it('should forward token', async () => {
expect.assertions(1);
server.use(
rest.get(`${mockBaseUrl}/locations`, (req, res, ctx) => {
expect(req.headers.get('authorization')).toBe(`Bearer ${token}`);
return res(ctx.json(defaultResponse));
}),
);
await client.getLocations({}, { token });
});
it('should not forward token if omitted', async () => {
expect.assertions(1);
server.use(
rest.get(`${mockBaseUrl}/locations`, (req, res, ctx) => {
expect(req.headers.get('authorization')).toBeNull();
return res(ctx.json(defaultResponse));
}),
);
await client.getLocations();
});
});
describe('getLocationById', () => {
const defaultResponse = {
data: {
@@ -36,6 +36,7 @@ import {
GetEntityAncestorsResponse,
GetEntityFacetsRequest,
GetEntityFacetsResponse,
GetLocationsResponse,
Location,
QueryEntitiesRequest,
QueryEntitiesResponse,
@@ -75,6 +76,21 @@ export class CatalogClient implements CatalogApi {
);
}
/**
* {@inheritdoc CatalogApi.getLocations}
*/
async getLocations(
request?: {},
options?: CatalogRequestOptions,
): Promise<GetLocationsResponse> {
const res = await this.requestRequired(
await this.apiClient.getLocations(request ?? {}, options),
);
return {
items: res.map(item => item.data),
};
}
/**
* {@inheritdoc CatalogApi.getLocationById}
*/
@@ -28,6 +28,7 @@ import {
GetEntityAncestorsResponse,
GetEntityFacetsRequest,
GetEntityFacetsResponse,
GetLocationsResponse,
Location,
QueryEntitiesRequest,
QueryEntitiesResponse,
@@ -228,6 +229,10 @@ export class InMemoryCatalogClient implements CatalogApi {
};
}
async getLocations(_request?: {}): Promise<GetLocationsResponse> {
throw new NotImplementedError('Method not implemented.');
}
async getLocationById(_id: string): Promise<Location | undefined> {
throw new NotImplementedError('Method not implemented.');
}
+20
View File
@@ -349,6 +349,15 @@ export type Location = {
target: string;
};
/**
* The response type for {@link CatalogClient.getLocations}
*
* @public
*/
export interface GetLocationsResponse {
items: Location[];
}
/**
* The request type for {@link CatalogClient.addLocation}.
*
@@ -590,6 +599,17 @@ export interface CatalogApi {
// Locations
/**
* List locations
*
* @param request - Request parameters
* @param options - Additional options
*/
getLocations(
request?: {},
options?: CatalogRequestOptions,
): Promise<GetLocationsResponse>;
/**
* Gets a registered location by its ID.
*
@@ -31,6 +31,7 @@ export type {
GetEntityAncestorsResponse,
GetEntityFacetsRequest,
GetEntityFacetsResponse,
GetLocationsResponse,
Location,
ValidateEntityResponse,
QueryEntitiesCursorRequest,
+2 -1
View File
@@ -51,12 +51,13 @@ export interface Config {
/**
* The credentials to use for requests. If multiple credentials are specified the first one that matches the organization is used.
* If not organization matches the first credential without an organization is used.
* If no organization matches the first credential without an organization is used.
*
* If no credentials are specified at all, either a default credential (for Azure DevOps) or anonymous access (for Azure DevOps Server) is used.
* @deepVisibility secret
*/
credentials?: {
organizations?: string[];
clientId?: string;
clientSecret?: string;
tenantId?: string;
@@ -44,6 +44,7 @@
"@backstage/cli": "workspace:^",
"@backstage/plugin-auth-backend": "workspace:^",
"@backstage/types": "workspace:^",
"@types/passport-github2": "^1.2.4",
"supertest": "^7.0.0"
},
"configSchema": "config.d.ts"
-58
View File
@@ -84,64 +84,6 @@ export interface Config {
};
};
/**
* The available auth-provider options and attributes
* @additionalProperties true
*/
providers?: {
/** @visibility frontend */
saml?: {
entryPoint: string;
logoutUrl?: string;
issuer: string;
/**
* @visibility secret
*/
cert: string;
audience?: string;
/**
* @visibility secret
*/
privateKey?: string;
authnContext?: string[];
identifierFormat?: string;
/**
* @visibility secret
*/
decryptionPvk?: string;
signatureAlgorithm?: 'sha256' | 'sha512';
digestAlgorithm?: string;
acceptedClockSkewMs?: number;
};
/** @visibility frontend */
auth0?: {
[authEnv: string]: {
clientId: string;
/**
* @visibility secret
*/
clientSecret: string;
domain: string;
callbackUrl?: string;
audience?: string;
connection?: string;
connectionScope?: string;
};
};
/** @visibility frontend */
onelogin?: {
[authEnv: string]: {
clientId: string;
/**
* @visibility secret
*/
clientSecret: string;
issuer: string;
callbackUrl?: string;
};
};
};
/**
* The backstage token expiration.
*/
-32
View File
@@ -1,34 +1,2 @@
# Knip report
## Unused dependencies (14)
| Name | Location | Severity |
| :---------------------- | :----------- | :------- |
| passport-google-oauth20 | package.json | error |
| passport-onelogin-oauth | package.json | error |
| google-auth-library | package.json | error |
| passport-microsoft | package.json | error |
| passport-github2 | package.json | error |
| passport-auth0 | package.json | error |
| openid-client | package.json | error |
| compression | package.json | error |
| node-cache | package.json | error |
| fs-extra | package.json | error |
| winston | package.json | error |
| morgan | package.json | error |
| cors | package.json | error |
| yn | package.json | error |
## Unused devDependencies (8)
| Name | Location | Severity |
| :----------------------------- | :----------- | :------- |
| @types/passport-google-oauth20 | package.json | error |
| @types/passport-microsoft | package.json | error |
| @types/passport-strategy | package.json | error |
| @types/passport-github2 | package.json | error |
| @types/passport-auth0 | package.json | error |
| @types/passport-saml | package.json | error |
| @types/body-parser | package.json | error |
| @types/xml2js | package.json | error |
+4 -47
View File
@@ -43,79 +43,36 @@
"test": "backstage-cli package test"
},
"dependencies": {
"@backstage/backend-common": "^0.25.0",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/catalog-client": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/plugin-auth-backend-module-atlassian-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-auth0-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-aws-alb-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-azure-easyauth-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-bitbucket-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-bitbucket-server-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-cloudflare-access-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-gcp-iap-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-github-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-gitlab-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-google-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-microsoft-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-oauth2-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-oidc-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-okta-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-onelogin-provider": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^",
"@backstage/types": "workspace:^",
"@google-cloud/firestore": "^7.0.0",
"@node-saml/passport-saml": "^5.0.0",
"@types/express": "^4.17.6",
"@types/passport": "^1.0.3",
"compression": "^1.7.4",
"connect-session-knex": "^4.0.0",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"express-session": "^1.17.1",
"fs-extra": "^11.2.0",
"google-auth-library": "^9.0.0",
"jose": "^5.0.0",
"knex": "^3.0.0",
"lodash": "^4.17.21",
"luxon": "^3.0.0",
"minimatch": "^9.0.0",
"morgan": "^1.10.0",
"node-cache": "^5.1.2",
"openid-client": "^5.2.1",
"passport": "^0.7.0",
"passport-auth0": "^1.4.3",
"passport-github2": "^0.1.12",
"passport-google-oauth20": "^2.0.0",
"passport-microsoft": "^1.0.0",
"passport-oauth2": "^1.6.1",
"passport-onelogin-oauth": "^0.0.1",
"uuid": "^11.0.0",
"winston": "^3.2.1",
"yn": "^4.0.0"
"uuid": "^11.0.0"
},
"devDependencies": {
"@backstage/backend-defaults": "workspace:^",
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@types/body-parser": "^1.19.0",
"@backstage/plugin-auth-backend-module-google-provider": "workspace:^",
"@types/cookie-parser": "^1.4.2",
"@types/express": "^4.17.6",
"@types/express-session": "^1.17.2",
"@types/passport-auth0": "^1.0.5",
"@types/passport-github2": "^1.2.4",
"@types/passport-google-oauth20": "^2.0.3",
"@types/passport-microsoft": "^1.0.0",
"@types/passport-saml": "^1.1.3",
"@types/passport-strategy": "^0.2.35",
"@types/xml2js": "^0.4.7",
"msw": "^1.0.0",
"@types/passport": "^1.0.3",
"supertest": "^7.0.0"
},
"configSchema": "config.d.ts"
-666
View File
@@ -3,675 +3,9 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { AuthOwnershipResolver } from '@backstage/plugin-auth-node';
import { AuthProviderConfig as AuthProviderConfig_2 } from '@backstage/plugin-auth-node';
import { AuthProviderFactory as AuthProviderFactory_2 } from '@backstage/plugin-auth-node';
import { AuthProviderRouteHandlers as AuthProviderRouteHandlers_2 } from '@backstage/plugin-auth-node';
import { AuthResolverCatalogUserQuery as AuthResolverCatalogUserQuery_2 } from '@backstage/plugin-auth-node';
import { AuthResolverContext as AuthResolverContext_2 } from '@backstage/plugin-auth-node';
import { AuthService } from '@backstage/backend-plugin-api';
import { AwsAlbResult as AwsAlbResult_2 } from '@backstage/plugin-auth-backend-module-aws-alb-provider';
import { AzureEasyAuthResult } from '@backstage/plugin-auth-backend-module-azure-easyauth-provider';
import { BackendFeature } from '@backstage/backend-plugin-api';
import { BackstageSignInResult } from '@backstage/plugin-auth-node';
import { CacheService } from '@backstage/backend-plugin-api';
import { CatalogApi } from '@backstage/catalog-client';
import { ClientAuthResponse } from '@backstage/plugin-auth-node';
import { cloudflareAccessSignInResolvers } from '@backstage/plugin-auth-backend-module-cloudflare-access-provider';
import { Config } from '@backstage/config';
import { CookieConfigurer as CookieConfigurer_2 } from '@backstage/plugin-auth-node';
import { DatabaseService } from '@backstage/backend-plugin-api';
import { decodeOAuthState } from '@backstage/plugin-auth-node';
import { DiscoveryService } from '@backstage/backend-plugin-api';
import { encodeOAuthState } from '@backstage/plugin-auth-node';
import { Entity } from '@backstage/catalog-model';
import express from 'express';
import { GcpIapResult as GcpIapResult_2 } from '@backstage/plugin-auth-backend-module-gcp-iap-provider';
import { GcpIapTokenInfo as GcpIapTokenInfo_2 } from '@backstage/plugin-auth-backend-module-gcp-iap-provider';
import { HttpAuthService } from '@backstage/backend-plugin-api';
import { LoggerService } from '@backstage/backend-plugin-api';
import { OAuth2ProxyResult as OAuth2ProxyResult_2 } from '@backstage/plugin-auth-backend-module-oauth2-proxy-provider';
import { OAuthEnvironmentHandler as OAuthEnvironmentHandler_2 } from '@backstage/plugin-auth-node';
import { OAuthState as OAuthState_2 } from '@backstage/plugin-auth-node';
import { OidcAuthResult as OidcAuthResult_2 } from '@backstage/plugin-auth-backend-module-oidc-provider';
import { prepareBackstageIdentityResponse as prepareBackstageIdentityResponse_2 } from '@backstage/plugin-auth-node';
import { Profile } from 'passport';
import { ProfileInfo as ProfileInfo_2 } from '@backstage/plugin-auth-node';
import { RootConfigService } from '@backstage/backend-plugin-api';
import { SignInInfo as SignInInfo_2 } from '@backstage/plugin-auth-node';
import { SignInResolver as SignInResolver_2 } from '@backstage/plugin-auth-node';
import { TokenManager } from '@backstage/backend-common';
import { TokenParams as TokenParams_2 } from '@backstage/plugin-auth-node';
import { UserEntity } from '@backstage/catalog-model';
import { WebMessageResponse as WebMessageResponse_2 } from '@backstage/plugin-auth-node';
// @public @deprecated
export type AuthHandler<TAuthResult> = (
input: TAuthResult,
context: AuthResolverContext_2,
) => Promise<AuthHandlerResult>;
// @public @deprecated
export type AuthHandlerResult = {
profile: ProfileInfo_2;
};
// @public
const authPlugin: BackendFeature;
export default authPlugin;
// @public @deprecated (undocumented)
export type AuthProviderConfig = AuthProviderConfig_2;
// @public @deprecated (undocumented)
export type AuthProviderFactory = AuthProviderFactory_2;
// @public @deprecated (undocumented)
export type AuthProviderRouteHandlers = AuthProviderRouteHandlers_2;
// @public @deprecated (undocumented)
export type AuthResolverCatalogUserQuery = AuthResolverCatalogUserQuery_2;
// @public @deprecated (undocumented)
export type AuthResolverContext = AuthResolverContext_2;
// @public @deprecated (undocumented)
export type AuthResponse<TProviderInfo> = ClientAuthResponse<TProviderInfo>;
// @public @deprecated
export type AwsAlbResult = AwsAlbResult_2;
// @public @deprecated (undocumented)
export type BitbucketOAuthResult = {
fullProfile: BitbucketPassportProfile;
params: {
id_token?: string;
scope: string;
expires_in: number;
};
accessToken: string;
refreshToken?: string;
};
// @public @deprecated (undocumented)
export type BitbucketPassportProfile = Profile & {
id?: string;
displayName?: string;
username?: string;
avatarUrl?: string;
_json?: {
links?: {
avatar?: {
href?: string;
};
};
};
};
// @public @deprecated (undocumented)
export type BitbucketServerOAuthResult = {
fullProfile: Profile;
params: {
scope: string;
access_token?: string;
token_type?: string;
expires_in?: number;
};
accessToken: string;
refreshToken?: string;
};
// @public @deprecated
export class CatalogIdentityClient {
constructor(options: {
catalogApi: CatalogApi;
tokenManager?: TokenManager;
discovery: DiscoveryService;
auth?: AuthService;
httpAuth?: HttpAuthService;
});
findUser(query: { annotations: Record<string, string> }): Promise<UserEntity>;
resolveCatalogMembership(query: {
entityRefs: string[];
logger?: LoggerService;
}): Promise<string[]>;
}
// @public @deprecated
export type CloudflareAccessClaims = {
aud: string[];
email: string;
exp: number;
iat: number;
nonce: string;
identity_nonce: string;
sub: string;
iss: string;
custom: string;
};
// @public @deprecated
export type CloudflareAccessGroup = {
id: string;
name: string;
email: string;
};
// @public @deprecated
export type CloudflareAccessIdentityProfile = {
id: string;
name: string;
email: string;
groups: CloudflareAccessGroup[];
};
// @public @deprecated (undocumented)
export type CloudflareAccessResult = {
claims: CloudflareAccessClaims;
cfIdentity: CloudflareAccessIdentityProfile;
expiresInSeconds?: number;
token: string;
};
// @public @deprecated (undocumented)
export type CookieConfigurer = CookieConfigurer_2;
// @public @deprecated
export function createAuthProviderIntegration<
TCreateOptions extends unknown[],
TResolvers extends {
[name in string]: (...args: any[]) => SignInResolver_2<any>;
},
>(config: {
create: (...args: TCreateOptions) => AuthProviderFactory_2;
resolvers?: TResolvers;
}): Readonly<{
create: (...args: TCreateOptions) => AuthProviderFactory_2;
resolvers: Readonly<string extends keyof TResolvers ? never : TResolvers>;
}>;
// @public @deprecated (undocumented)
export function createOriginFilter(config: Config): (origin: string) => boolean;
// @public @deprecated (undocumented)
export function createRouter(options: RouterOptions): Promise<express.Router>;
// @public @deprecated
export const defaultAuthProviderFactories: {
[providerId: string]: AuthProviderFactory_2;
};
// @public @deprecated (undocumented)
export type EasyAuthResult = AzureEasyAuthResult;
// @public @deprecated (undocumented)
export const encodeState: typeof encodeOAuthState;
// @public @deprecated (undocumented)
export const ensuresXRequestedWith: (req: express.Request) => boolean;
// @public @deprecated
export type GcpIapResult = GcpIapResult_2;
// @public @deprecated
export type GcpIapTokenInfo = GcpIapTokenInfo_2;
// @public @deprecated
export function getDefaultOwnershipEntityRefs(entity: Entity): string[];
// @public @deprecated (undocumented)
export type GithubOAuthResult = {
fullProfile: Profile;
params: {
scope: string;
expires_in?: string;
refresh_token_expires_in?: string;
};
accessToken: string;
refreshToken?: string;
};
// @public @deprecated (undocumented)
export type OAuth2ProxyResult = OAuth2ProxyResult_2;
// @public @deprecated (undocumented)
export class OAuthAdapter implements AuthProviderRouteHandlers_2 {
constructor(handlers: OAuthHandlers, options: OAuthAdapterOptions);
// (undocumented)
frameHandler(req: express.Request, res: express.Response): Promise<void>;
// (undocumented)
static fromConfig(
config: AuthProviderConfig_2,
handlers: OAuthHandlers,
options: Pick<
OAuthAdapterOptions,
'providerId' | 'persistScopes' | 'callbackUrl'
>,
): OAuthAdapter;
// (undocumented)
logout(req: express.Request, res: express.Response): Promise<void>;
// (undocumented)
refresh(req: express.Request, res: express.Response): Promise<void>;
// (undocumented)
start(req: express.Request, res: express.Response): Promise<void>;
}
// @public @deprecated (undocumented)
export type OAuthAdapterOptions = {
providerId: string;
persistScopes?: boolean;
appOrigin: string;
baseUrl: string;
cookieConfigurer: CookieConfigurer_2;
isOriginAllowed: (origin: string) => boolean;
callbackUrl: string;
};
// @public @deprecated (undocumented)
export const OAuthEnvironmentHandler: typeof OAuthEnvironmentHandler_2;
// @public @deprecated (undocumented)
export interface OAuthHandlers {
handler(req: express.Request): Promise<{
response: OAuthResponse;
refreshToken?: string;
}>;
logout?(req: OAuthLogoutRequest): Promise<void>;
refresh?(req: OAuthRefreshRequest): Promise<{
response: OAuthResponse;
refreshToken?: string;
}>;
start(req: OAuthStartRequest): Promise<OAuthStartResponse>;
}
// @public @deprecated (undocumented)
export type OAuthLogoutRequest = express.Request<{}> & {
refreshToken: string;
};
// @public @deprecated (undocumented)
export type OAuthProviderInfo = {
accessToken: string;
idToken?: string;
expiresInSeconds?: number;
scope: string;
};
// @public @deprecated
export type OAuthProviderOptions = {
clientId: string;
clientSecret: string;
callbackUrl: string;
};
// @public @deprecated (undocumented)
export type OAuthRefreshRequest = express.Request<{}> & {
scope: string;
refreshToken: string;
};
// @public @deprecated (undocumented)
export type OAuthResponse = {
profile: ProfileInfo_2;
providerInfo: OAuthProviderInfo;
backstageIdentity?: BackstageSignInResult;
};
// @public @deprecated (undocumented)
export type OAuthResult = {
fullProfile: Profile;
params: {
id_token?: string;
scope: string;
token_type?: string;
expires_in: number;
};
accessToken: string;
refreshToken?: string;
};
// @public @deprecated (undocumented)
export type OAuthStartRequest = express.Request<{}> & {
scope: string;
state: OAuthState;
};
// @public @deprecated (undocumented)
export type OAuthStartResponse = {
url: string;
status?: number;
};
// @public @deprecated (undocumented)
export type OAuthState = OAuthState_2;
// @public @deprecated (undocumented)
export type OidcAuthResult = OidcAuthResult_2;
// @public @deprecated (undocumented)
export const postMessageResponse: (
res: express.Response,
appOrigin: string,
response: WebMessageResponse,
) => void;
// @public @deprecated (undocumented)
export const prepareBackstageIdentityResponse: typeof prepareBackstageIdentityResponse_2;
// @public @deprecated (undocumented)
export type ProfileInfo = ProfileInfo_2;
// @public @deprecated (undocumented)
export type ProviderFactories = {
[s: string]: AuthProviderFactory_2;
};
// @public @deprecated
export const providers: Readonly<{
atlassian: Readonly<{
create: (
options?:
| {
authHandler?: AuthHandler<OAuthResult>;
signIn?: {
resolver: SignInResolver_2<OAuthResult>;
};
}
| undefined,
) => AuthProviderFactory_2;
resolvers: never;
}>;
auth0: Readonly<{
create: (
options?:
| {
authHandler?: AuthHandler<OAuthResult>;
signIn?: {
resolver: SignInResolver_2<OAuthResult>;
};
}
| undefined,
) => AuthProviderFactory_2;
resolvers: never;
}>;
awsAlb: Readonly<{
create: (
options?:
| {
authHandler?: AuthHandler<AwsAlbResult_2>;
signIn: {
resolver: SignInResolver_2<AwsAlbResult_2>;
};
}
| undefined,
) => AuthProviderFactory_2;
resolvers: never;
}>;
bitbucket: Readonly<{
create: (
options?:
| {
authHandler?: AuthHandler<OAuthResult>;
signIn?: {
resolver: SignInResolver_2<OAuthResult>;
};
}
| undefined,
) => AuthProviderFactory_2;
resolvers: Readonly<{
userIdMatchingUserEntityAnnotation: () => SignInResolver_2<OAuthResult>;
usernameMatchingUserEntityAnnotation: () => SignInResolver_2<OAuthResult>;
}>;
}>;
bitbucketServer: Readonly<{
create: (
options?:
| {
authHandler?: AuthHandler<BitbucketServerOAuthResult>;
signIn?: {
resolver: SignInResolver_2<BitbucketServerOAuthResult>;
};
}
| undefined,
) => AuthProviderFactory_2;
resolvers: Readonly<{
emailMatchingUserEntityProfileEmail: () => SignInResolver_2<BitbucketServerOAuthResult>;
}>;
}>;
cfAccess: Readonly<{
create: (options: {
authHandler?: AuthHandler<CloudflareAccessResult>;
signIn: {
resolver: SignInResolver_2<CloudflareAccessResult>;
};
cache?: CacheService;
}) => AuthProviderFactory_2;
resolvers: Readonly<cloudflareAccessSignInResolvers>;
}>;
gcpIap: Readonly<{
create: (options: {
authHandler?: AuthHandler<GcpIapResult>;
signIn: {
resolver: SignInResolver_2<GcpIapResult>;
};
}) => AuthProviderFactory_2;
resolvers: never;
}>;
github: Readonly<{
create: (
options?:
| {
authHandler?: AuthHandler<GithubOAuthResult>;
signIn?: {
resolver: SignInResolver_2<GithubOAuthResult>;
};
stateEncoder?: StateEncoder;
}
| undefined,
) => AuthProviderFactory_2;
resolvers: Readonly<{
usernameMatchingUserEntityName: () => SignInResolver_2<GithubOAuthResult>;
}>;
}>;
gitlab: Readonly<{
create: (
options?:
| {
authHandler?: AuthHandler<OAuthResult>;
signIn?: {
resolver: SignInResolver_2<OAuthResult>;
};
}
| undefined,
) => AuthProviderFactory_2;
resolvers: never;
}>;
google: Readonly<{
create: (
options?:
| {
authHandler?: AuthHandler<OAuthResult>;
signIn?: {
resolver: SignInResolver_2<OAuthResult>;
};
}
| undefined,
) => AuthProviderFactory_2;
resolvers: Readonly<{
emailMatchingUserEntityProfileEmail: () => SignInResolver_2<OAuthResult>;
emailLocalPartMatchingUserEntityName: () => SignInResolver_2<OAuthResult>;
emailMatchingUserEntityAnnotation: () => SignInResolver_2<OAuthResult>;
}>;
}>;
microsoft: Readonly<{
create: (
options?:
| {
authHandler?: AuthHandler<OAuthResult>;
signIn?: {
resolver: SignInResolver_2<OAuthResult>;
};
}
| undefined,
) => AuthProviderFactory_2;
resolvers: Readonly<{
emailMatchingUserEntityProfileEmail: () => SignInResolver_2<OAuthResult>;
emailLocalPartMatchingUserEntityName: () => SignInResolver_2<OAuthResult>;
userIdMatchingUserEntityAnnotation: () => SignInResolver_2<OAuthResult>;
emailMatchingUserEntityAnnotation: () => SignInResolver_2<OAuthResult>;
}>;
}>;
oauth2: Readonly<{
create: (
options?:
| {
authHandler?: AuthHandler<OAuthResult>;
signIn?: {
resolver: SignInResolver_2<OAuthResult>;
};
}
| undefined,
) => AuthProviderFactory_2;
resolvers: never;
}>;
oauth2Proxy: Readonly<{
create: (options: {
authHandler?: AuthHandler<OAuth2ProxyResult_2>;
signIn: {
resolver: SignInResolver_2<OAuth2ProxyResult_2>;
};
}) => AuthProviderFactory_2;
resolvers: never;
}>;
oidc: Readonly<{
create: (
options?:
| {
authHandler?: AuthHandler<OidcAuthResult_2>;
signIn?: {
resolver: SignInResolver_2<OidcAuthResult_2>;
};
}
| undefined,
) => AuthProviderFactory_2;
resolvers: Readonly<{
emailLocalPartMatchingUserEntityName: () => SignInResolver_2<unknown>;
emailMatchingUserEntityProfileEmail: () => SignInResolver_2<unknown>;
}>;
}>;
okta: Readonly<{
create: (
options?:
| {
authHandler?: AuthHandler<OAuthResult>;
signIn?: {
resolver: SignInResolver_2<OAuthResult>;
};
}
| undefined,
) => AuthProviderFactory_2;
resolvers: Readonly<{
emailLocalPartMatchingUserEntityName: () => SignInResolver_2<unknown>;
emailMatchingUserEntityProfileEmail: () => SignInResolver_2<unknown>;
emailMatchingUserEntityAnnotation(): SignInResolver_2<OAuthResult>;
}>;
}>;
onelogin: Readonly<{
create: (
options?:
| {
authHandler?: AuthHandler<OAuthResult>;
signIn?: {
resolver: SignInResolver_2<OAuthResult>;
};
}
| undefined,
) => AuthProviderFactory_2;
resolvers: never;
}>;
saml: Readonly<{
create: (
options?:
| {
authHandler?: AuthHandler<SamlAuthResult>;
signIn?: {
resolver: SignInResolver_2<SamlAuthResult>;
};
}
| undefined,
) => AuthProviderFactory_2;
resolvers: Readonly<{
nameIdMatchingUserEntityName(): SignInResolver_2<SamlAuthResult>;
}>;
}>;
easyAuth: Readonly<{
create: (
options?:
| {
authHandler?: AuthHandler<AzureEasyAuthResult>;
signIn: {
resolver: SignInResolver_2<AzureEasyAuthResult>;
};
}
| undefined,
) => AuthProviderFactory_2;
resolvers: never;
}>;
}>;
// @public @deprecated (undocumented)
export const readState: typeof decodeOAuthState;
// @public @deprecated (undocumented)
export interface RouterOptions {
// (undocumented)
auth?: AuthService;
// (undocumented)
catalogApi?: CatalogApi;
// (undocumented)
config: RootConfigService;
// (undocumented)
database: DatabaseService;
// (undocumented)
disableDefaultProviderFactories?: boolean;
// (undocumented)
discovery: DiscoveryService;
// (undocumented)
httpAuth?: HttpAuthService;
// (undocumented)
logger: LoggerService;
// (undocumented)
ownershipResolver?: AuthOwnershipResolver;
// (undocumented)
providerFactories?: ProviderFactories;
// (undocumented)
tokenFactoryAlgorithm?: string;
// (undocumented)
tokenManager?: TokenManager;
}
// @public @deprecated (undocumented)
export type SamlAuthResult = {
fullProfile: any;
};
// @public @deprecated (undocumented)
export type SignInInfo<TAuthResult> = SignInInfo_2<TAuthResult>;
// @public @deprecated (undocumented)
export type SignInResolver<TAuthResult> = SignInResolver_2<TAuthResult>;
// @public @deprecated (undocumented)
export type StateEncoder = (req: OAuthStartRequest) => Promise<{
encodedState: string;
}>;
// @public @deprecated (undocumented)
export type TokenParams = TokenParams_2;
// @public @deprecated (undocumented)
export const verifyNonce: (req: express.Request, providerId: string) => void;
// @public @deprecated (undocumented)
export type WebMessageResponse = WebMessageResponse_2;
```
+4 -8
View File
@@ -24,7 +24,7 @@ import {
AuthProviderFactory,
authProvidersExtensionPoint,
} from '@backstage/plugin-auth-node';
import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha';
import { catalogServiceRef } from '@backstage/plugin-catalog-node';
import { createRouter } from './service/router';
/**
@@ -66,8 +66,7 @@ export const authPlugin = createBackendPlugin({
database: coreServices.database,
discovery: coreServices.discovery,
auth: coreServices.auth,
httpAuth: coreServices.httpAuth,
catalogApi: catalogServiceRef,
catalog: catalogServiceRef,
},
async init({
httpRouter,
@@ -76,8 +75,7 @@ export const authPlugin = createBackendPlugin({
database,
discovery,
auth,
httpAuth,
catalogApi,
catalog,
}) {
const router = await createRouter({
logger,
@@ -85,10 +83,8 @@ export const authPlugin = createBackendPlugin({
database,
discovery,
auth,
httpAuth,
catalogApi,
catalog,
providerFactories: Object.fromEntries(providers),
disableDefaultProviderFactories: true,
ownershipResolver,
});
httpRouter.addAuthPolicy({
@@ -14,12 +14,10 @@
* limitations under the License.
*/
import { DatabaseManager } from '@backstage/backend-common';
import {
DatabaseService,
resolvePackagePath,
} from '@backstage/backend-plugin-api';
import { ConfigReader } from '@backstage/config';
import { Knex } from 'knex';
const migrationsDir = resolvePackagePath(
@@ -39,21 +37,6 @@ export class AuthDatabase {
return new AuthDatabase(database);
}
/** @internal */
static forTesting(): AuthDatabase {
const config = new ConfigReader({
backend: {
database: {
client: 'better-sqlite3',
connection: ':memory:',
useNullAsDefault: true,
},
},
});
const database = DatabaseManager.fromConfig(config).forPlugin('auth');
return new AuthDatabase(database);
}
static async runMigrations(knex: Knex): Promise<void> {
await knex.migrate.latest({
directory: migrationsDir,
@@ -20,9 +20,13 @@ import { DatabaseKeyStore } from './DatabaseKeyStore';
import { FirestoreKeyStore } from './FirestoreKeyStore';
import { KeyStores } from './KeyStores';
import { MemoryKeyStore } from './MemoryKeyStore';
import { mockServices } from '@backstage/backend-test-utils';
import { mockServices, TestDatabases } from '@backstage/backend-test-utils';
jest.setTimeout(60_000);
describe('KeyStores', () => {
const databases = TestDatabases.create();
const defaultConfigOptions = {
auth: {
keyStore: {
@@ -32,65 +36,77 @@ describe('KeyStores', () => {
};
const defaultConfig = new ConfigReader(defaultConfigOptions);
it('reads auth section from config', async () => {
const configSpy = jest.spyOn(defaultConfig, 'getOptionalConfig');
const keyStore = await KeyStores.fromConfig(defaultConfig, {
logger: mockServices.logger.mock(),
database: AuthDatabase.forTesting(),
});
it.each(databases.eachSupportedId())(
'reads auth section from config, %p',
async databaseId => {
const knex = await databases.init(databaseId);
const configSpy = jest.spyOn(defaultConfig, 'getOptionalConfig');
const keyStore = await KeyStores.fromConfig(defaultConfig, {
logger: mockServices.logger.mock(),
database: AuthDatabase.create(mockServices.database({ knex })),
});
expect(keyStore).toBeInstanceOf(MemoryKeyStore);
expect(configSpy).toHaveBeenCalledWith('auth.keyStore');
expect(
defaultConfig
.getOptionalConfig('auth.keyStore')
?.getOptionalString('provider'),
).toBe(defaultConfigOptions.auth.keyStore.provider);
});
expect(keyStore).toBeInstanceOf(MemoryKeyStore);
expect(configSpy).toHaveBeenCalledWith('auth.keyStore');
expect(
defaultConfig
.getOptionalConfig('auth.keyStore')
?.getOptionalString('provider'),
).toBe(defaultConfigOptions.auth.keyStore.provider);
},
);
it('can handle without auth config', async () => {
const keyStore = await KeyStores.fromConfig(new ConfigReader({}), {
logger: mockServices.logger.mock(),
database: AuthDatabase.forTesting(),
});
expect(keyStore).toBeInstanceOf(DatabaseKeyStore);
});
it.each(databases.eachSupportedId())(
'can handle without auth config, %p',
async databaseId => {
const knex = await databases.init(databaseId);
const keyStore = await KeyStores.fromConfig(new ConfigReader({}), {
logger: mockServices.logger.mock(),
database: AuthDatabase.create(mockServices.database({ knex })),
});
expect(keyStore).toBeInstanceOf(DatabaseKeyStore);
},
);
it('can handle additional provider config', async () => {
jest.spyOn(FirestoreKeyStore, 'verifyConnection').mockResolvedValue();
const createSpy = jest.spyOn(FirestoreKeyStore, 'create');
it.each(databases.eachSupportedId())(
'can handle additional provider config, %p',
async databaseId => {
const knex = await databases.init(databaseId);
jest.spyOn(FirestoreKeyStore, 'verifyConnection').mockResolvedValue();
const createSpy = jest.spyOn(FirestoreKeyStore, 'create');
const configOptions = {
auth: {
keyStore: {
provider: 'firestore',
firestore: {
projectId: 'my-project',
keyFilename: 'cred.json',
path: 'my-path',
timeout: 100,
host: 'localhost',
port: 8088,
ssl: false,
const configOptions = {
auth: {
keyStore: {
provider: 'firestore',
firestore: {
projectId: 'my-project',
keyFilename: 'cred.json',
path: 'my-path',
timeout: 100,
host: 'localhost',
port: 8088,
ssl: false,
},
},
},
},
};
const config = new ConfigReader(configOptions);
const keyStore = await KeyStores.fromConfig(config, {
logger: mockServices.logger.mock(),
database: AuthDatabase.forTesting(),
});
};
const config = new ConfigReader(configOptions);
const keyStore = await KeyStores.fromConfig(config, {
logger: mockServices.logger.mock(),
database: AuthDatabase.create(mockServices.database({ knex })),
});
expect(keyStore).toBeInstanceOf(FirestoreKeyStore);
expect(createSpy).toHaveBeenCalledWith(
configOptions.auth.keyStore.firestore,
);
expect(
config
.getOptionalConfig('auth.keyStore')
?.getOptionalConfig('firestore')
?.getOptionalString('projectId'),
).toBe(configOptions.auth.keyStore.firestore.projectId);
});
expect(keyStore).toBeInstanceOf(FirestoreKeyStore);
expect(createSpy).toHaveBeenCalledWith(
configOptions.auth.keyStore.firestore,
);
expect(
config
.getOptionalConfig('auth.keyStore')
?.getOptionalConfig('firestore')
?.getOptionalString('projectId'),
).toBe(configOptions.auth.keyStore.firestore.projectId);
},
);
});
@@ -1,24 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { bindOidcRouter } from './router';
export { TokenFactory } from './TokenFactory';
export { DatabaseKeyStore } from './DatabaseKeyStore';
export { MemoryKeyStore } from './MemoryKeyStore';
export { FirestoreKeyStore } from './FirestoreKeyStore';
export { KeyStores } from './KeyStores';
export type { KeyStore, TokenParams } from './types';
export { UserInfoDatabaseHandler } from './UserInfoDatabaseHandler';
+2 -8
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { TokenParams as _TokenParams } from '@backstage/plugin-auth-node';
import { TokenParams } from '@backstage/plugin-auth-node';
/** Represents any form of serializable JWK */
export interface AnyJWK extends Record<string, string> {
@@ -24,12 +24,6 @@ export interface AnyJWK extends Record<string, string> {
kty: string;
}
/**
* @public
* @deprecated import from `@backstage/plugin-auth-node` instead
*/
export type TokenParams = _TokenParams;
/**
* A TokenIssuer is able to issue verifiable ID Tokens on demand.
*/
@@ -37,7 +31,7 @@ export type TokenIssuer = {
/**
* Issues a new ID Token
*/
issueToken(params: _TokenParams): Promise<string>;
issueToken(params: TokenParams): Promise<string>;
/**
* List all public keys that are currently being used to sign tokens, or have been used
-14
View File
@@ -21,17 +21,3 @@
*/
export { authPlugin as default } from './authPlugin';
export * from './service';
export type { TokenParams } from './identity';
export * from './providers';
// flow package provides 2 functions
// ensuresXRequestedWith and postMessageResponse to safely handle CORS requests for login. The WebMessageResponse type in flow is used to type the response from the login-popup
export * from './lib/flow';
// OAuth wrapper over a passport or a custom `strategy`.
export * from './lib/oauth';
export * from './lib/catalog';
export { getDefaultOwnershipEntityRefs } from './lib/resolvers';
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { TokenManager } from '@backstage/backend-common';
import {
RELATION_MEMBER_OF,
UserEntityV1alpha1,
@@ -24,15 +23,12 @@ import { CatalogIdentityClient } from './CatalogIdentityClient';
import { mockServices } from '@backstage/backend-test-utils';
describe('CatalogIdentityClient', () => {
const tokenManager: jest.Mocked<TokenManager> = {
getToken: jest.fn(),
authenticate: jest.fn(),
};
const auth = mockServices.auth({ pluginId: 'auth' });
afterEach(() => jest.resetAllMocks());
it('findUser passes through the correct search params', async () => {
const catalogApi = catalogServiceMock({
const catalog = catalogServiceMock({
entities: [
{
apiVersion: 'backstage.io/v1beta1',
@@ -46,27 +42,26 @@ describe('CatalogIdentityClient', () => {
},
],
});
jest.spyOn(catalogApi, 'getEntities');
jest.spyOn(catalog, 'getEntities');
tokenManager.getToken.mockResolvedValue({ token: 'my-token' });
const client = new CatalogIdentityClient({
discovery: mockServices.discovery(),
catalogApi,
tokenManager,
catalog,
auth,
});
await client.findUser({ annotations: { key: 'value' } });
expect(catalogApi.getEntities).toHaveBeenCalledWith(
expect(catalog.getEntities).toHaveBeenCalledWith(
{
filter: {
kind: 'user',
'metadata.annotations.key': 'value',
},
},
{ token: 'my-token' },
{
credentials: await auth.getOwnServiceCredentials(),
},
);
expect(tokenManager.getToken).toHaveBeenCalledWith();
});
it('resolveCatalogMembership resolves membership', async () => {
@@ -105,21 +100,19 @@ describe('CatalogIdentityClient', () => {
],
},
];
const catalogApi = catalogServiceMock({ entities: mockUsers });
jest.spyOn(catalogApi, 'getEntities');
tokenManager.getToken.mockResolvedValue({ token: 'my-token' });
const catalog = catalogServiceMock({ entities: mockUsers });
jest.spyOn(catalog, 'getEntities');
const client = new CatalogIdentityClient({
discovery: {} as any,
catalogApi,
tokenManager,
catalog,
auth,
});
const claims = await client.resolveCatalogMembership({
entityRefs: ['inigom', 'User:default/imontoya', 'User:reality/mpatinkin'],
});
expect(catalogApi.getEntities).toHaveBeenCalledWith(
expect(catalog.getEntities).toHaveBeenCalledWith(
{
filter: [
{
@@ -139,7 +132,9 @@ describe('CatalogIdentityClient', () => {
},
],
},
{ token: 'my-token' },
{
credentials: await auth.getOwnServiceCredentials(),
},
);
expect(claims).toMatchObject([
@@ -14,14 +14,9 @@
* limitations under the License.
*/
import {
AuthService,
DiscoveryService,
HttpAuthService,
LoggerService,
} from '@backstage/backend-plugin-api';
import { AuthService, LoggerService } from '@backstage/backend-plugin-api';
import { ConflictError, NotFoundError } from '@backstage/errors';
import { CatalogApi } from '@backstage/catalog-client';
import { CatalogService } from '@backstage/plugin-catalog-node';
import {
CompoundEntityRef,
parseEntityRef,
@@ -29,38 +24,17 @@ import {
stringifyEntityRef,
UserEntity,
} from '@backstage/catalog-model';
import {
TokenManager,
createLegacyAuthAdapters,
} from '@backstage/backend-common';
/**
* A catalog client tailored for reading out identity data from the catalog.
*
* @public
* @deprecated Use the provided `AuthResolverContext` instead, see https://backstage.io/docs/auth/identity-resolver#building-custom-resolvers
*/
export class CatalogIdentityClient {
private readonly catalogApi: CatalogApi;
private readonly catalog: CatalogService;
private readonly auth: AuthService;
constructor(options: {
catalogApi: CatalogApi;
tokenManager?: TokenManager;
discovery: DiscoveryService;
auth?: AuthService;
httpAuth?: HttpAuthService;
}) {
this.catalogApi = options.catalogApi;
const { auth } = createLegacyAuthAdapters({
auth: options.auth,
httpAuth: options.httpAuth,
discovery: options.discovery,
tokenManager: options.tokenManager,
});
this.auth = auth;
constructor(options: { catalog: CatalogService; auth: AuthService }) {
this.catalog = options.catalog;
this.auth = options.auth;
}
/**
@@ -78,12 +52,10 @@ export class CatalogIdentityClient {
filter[`metadata.annotations.${key}`] = value;
}
const { token } = await this.auth.getPluginRequestToken({
onBehalfOf: await this.auth.getOwnServiceCredentials(),
targetPluginId: 'catalog',
});
const { items } = await this.catalogApi.getEntities({ filter }, { token });
const { items } = await this.catalog.getEntities(
{ filter },
{ credentials: await this.auth.getOwnServiceCredentials() },
);
if (items.length !== 1) {
if (items.length > 1) {
@@ -129,13 +101,11 @@ export class CatalogIdentityClient {
'metadata.name': ref.name,
}));
const { token } = await this.auth.getPluginRequestToken({
onBehalfOf: await this.auth.getOwnServiceCredentials(),
targetPluginId: 'catalog',
});
const entities = await this.catalogApi
.getEntities({ filter }, { token })
const entities = await this.catalog
.getEntities(
{ filter },
{ credentials: await this.auth.getOwnServiceCredentials() },
)
.then(r => r.items);
if (entityRefs.length !== entities.length) {
@@ -1,17 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { CatalogIdentityClient } from './CatalogIdentityClient';
@@ -1,203 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import express from 'express';
import {
safelyEncodeURIComponent,
ensuresXRequestedWith,
postMessageResponse,
} from './authFlowHelpers';
import { WebMessageResponse } from './types';
describe('oauth helpers', () => {
describe('safelyEncodeURIComponent', () => {
it('encodes all occurrences of single quotes', () => {
expect(safelyEncodeURIComponent("a'ö'b")).toBe('a%27%C3%B6%27b');
});
});
describe('postMessageResponse', () => {
const appOrigin = 'http://localhost:3000';
it('should post a message back with payload success', () => {
const mockResponse = {
end: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
} as unknown as express.Response;
const data: WebMessageResponse = {
type: 'authorization_response',
response: {
providerInfo: {
accessToken: 'ACCESS_TOKEN',
idToken: 'ID_TOKEN',
expiresInSeconds: 10,
scope: 'email',
},
profile: {
email: 'foo@bar.com',
},
backstageIdentity: {
token: 'a.b.c',
identity: {
type: 'user',
ownershipEntityRefs: [],
userEntityRef: 'a',
},
},
},
};
const encoded = safelyEncodeURIComponent(JSON.stringify(data));
postMessageResponse(mockResponse, appOrigin, data);
expect(mockResponse.setHeader).toHaveBeenCalledTimes(3);
expect(mockResponse.end).toHaveBeenCalledTimes(1);
expect(mockResponse.end).toHaveBeenCalledWith(
expect.stringContaining(encoded),
);
});
it('should post a message back with payload error', () => {
const mockResponse = {
end: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
} as unknown as express.Response;
const data: WebMessageResponse = {
type: 'authorization_response',
error: new Error('Unknown error occurred'),
};
const encoded = safelyEncodeURIComponent(JSON.stringify(data));
postMessageResponse(mockResponse, appOrigin, data);
expect(mockResponse.setHeader).toHaveBeenCalledTimes(3);
expect(mockResponse.end).toHaveBeenCalledTimes(1);
expect(mockResponse.end).toHaveBeenCalledWith(
expect.stringContaining(encoded),
);
});
it('should call postMessage twice but only one of them with target *', () => {
let responseBody = '';
const mockResponse = {
end: jest.fn(body => {
responseBody = body;
return this;
}),
setHeader: jest.fn().mockReturnThis(),
} as unknown as express.Response;
const data: WebMessageResponse = {
type: 'authorization_response',
response: {
providerInfo: {
accessToken: 'ACCESS_TOKEN',
idToken: 'ID_TOKEN',
expiresInSeconds: 10,
scope: 'email',
},
profile: {
email: 'foo@bar.com',
},
backstageIdentity: {
token: 'a.b.c',
identity: {
type: 'user',
ownershipEntityRefs: [],
userEntityRef: 'a',
},
},
},
};
postMessageResponse(mockResponse, appOrigin, data);
expect(responseBody.match(/.postMessage\(/g)).toHaveLength(2);
expect(
responseBody.match(/.postMessage\([a-zA-Z.()]*, \'\*\'\)/g),
).toHaveLength(1);
const errData: WebMessageResponse = {
type: 'authorization_response',
error: new Error('Unknown error occurred'),
};
postMessageResponse(mockResponse, appOrigin, errData);
expect(responseBody.match(/.postMessage\(/g)).toHaveLength(2);
expect(
responseBody.match(/.postMessage\([a-zA-Z.()]*, \'\*\'\)/g),
).toHaveLength(1);
});
it('handles single quotes and unicode chars safely', () => {
const mockResponse = {
end: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
} as unknown as express.Response;
const data: WebMessageResponse = {
type: 'authorization_response',
response: {
providerInfo: {
accessToken: 'ACCESS_TOKEN',
idToken: 'ID_TOKEN',
expiresInSeconds: 10,
scope: 'email',
},
profile: {
email: 'foo@bar.com',
displayName: "Adam l'Hôpital",
},
backstageIdentity: {
token: 'a.b.c',
identity: {
type: 'user',
ownershipEntityRefs: [],
userEntityRef: 'a',
},
},
},
};
postMessageResponse(mockResponse, appOrigin, data);
expect(mockResponse.setHeader).toHaveBeenCalledTimes(3);
expect(mockResponse.end).toHaveBeenCalledTimes(1);
expect(mockResponse.end).toHaveBeenCalledWith(
expect.stringContaining('Adam%20l%27H%C3%B4pital'),
);
});
});
describe('ensuresXRequestedWith', () => {
it('should return false if no header present', () => {
const mockRequest = {
header: () => jest.fn(),
} as unknown as express.Request;
expect(ensuresXRequestedWith(mockRequest)).toBe(false);
});
it('should return false if header present with incorrect value', () => {
const mockRequest = {
header: () => 'INVALID',
} as unknown as express.Request;
expect(ensuresXRequestedWith(mockRequest)).toBe(false);
});
it('should return true if header present with correct value', () => {
const mockRequest = {
header: () => 'XMLHttpRequest',
} as unknown as express.Request;
expect(ensuresXRequestedWith(mockRequest)).toBe(true);
});
});
});
@@ -1,85 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import express from 'express';
import crypto from 'crypto';
import { WebMessageResponse } from './types';
export const safelyEncodeURIComponent = (value: string) => {
// Note the g at the end of the regex; all occurrences of single quotes must
// be replaced, which encodeURIComponent does not do itself by default
return encodeURIComponent(value).replace(/'/g, '%27');
};
/**
* @public
* @deprecated Use `sendWebMessageResponse` from `@backstage/plugin-auth-node` instead
*/
export const postMessageResponse = (
res: express.Response,
appOrigin: string,
response: WebMessageResponse,
) => {
const jsonData = JSON.stringify(response);
const base64Data = safelyEncodeURIComponent(jsonData);
const base64Origin = safelyEncodeURIComponent(appOrigin);
// NOTE: It is absolutely imperative that we use the safe encoder above, to
// be sure that the js code below does not allow the injection of malicious
// data.
// TODO: Make target app origin configurable globally
//
// postMessage fails silently if the targetOrigin is disallowed.
// So 2 postMessages are sent from the popup to the parent window.
// First, the origin being used to post the actual authorization response is
// shared with the parent window with a postMessage with targetOrigin '*'.
// Second, the actual authorization response is sent with the app origin
// as the targetOrigin.
// If the first message was received but the actual auth response was
// never received, the event listener can conclude that targetOrigin
// was disallowed, indicating potential misconfiguration.
//
const script = `
var authResponse = decodeURIComponent('${base64Data}');
var origin = decodeURIComponent('${base64Origin}');
var originInfo = {'type': 'config_info', 'targetOrigin': origin};
(window.opener || window.parent).postMessage(originInfo, '*');
(window.opener || window.parent).postMessage(JSON.parse(authResponse), origin);
setTimeout(() => {
window.close();
}, 100); // same as the interval of the core-app-api lib/loginPopup.ts (to address race conditions)
`;
const hash = crypto.createHash('sha256').update(script).digest('base64');
res.setHeader('Content-Type', 'text/html');
res.setHeader('X-Frame-Options', 'sameorigin');
res.setHeader('Content-Security-Policy', `script-src 'sha256-${hash}'`);
res.end(`<html><body><script>${script}</script></body></html>`);
};
/**
* @public
* @deprecated Use inline logic to check that the `X-Requested-With` header is set to `'XMLHttpRequest'` instead.
*/
export const ensuresXRequestedWith = (req: express.Request) => {
const requiredHeader = req.header('X-Requested-With');
if (!requiredHeader || requiredHeader !== 'XMLHttpRequest') {
return false;
}
return true;
};
@@ -1,19 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { ensuresXRequestedWith, postMessageResponse } from './authFlowHelpers';
export type { WebMessageResponse } from './types';
@@ -1,23 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { WebMessageResponse as _WebMessageResponse } from '@backstage/plugin-auth-node';
/**
* @public
* @deprecated import from `@backstage/plugin-auth-node` instead
*/
export type WebMessageResponse = _WebMessageResponse;
@@ -1,61 +0,0 @@
/*
* Copyright 2023 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 { AuthResolverContext } from '@backstage/plugin-auth-node';
import { AuthHandler } from '../../providers';
import { OAuthResult } from '../oauth';
import { PassportProfile } from '../passport/types';
import { adaptLegacyOAuthHandler } from './adaptLegacyOAuthHandler';
describe('adaptLegacyOAuthHandler', () => {
it('should pass through undefined', () => {
expect(adaptLegacyOAuthHandler(undefined)).toBeUndefined();
});
it('should convert an old auth handler to a new profile transform', () => {
const authHandler: AuthHandler<OAuthResult> = jest.fn();
const profileTransform = adaptLegacyOAuthHandler(authHandler);
profileTransform?.(
{
fullProfile: { id: 'id' } as PassportProfile,
session: {
accessToken: 'token',
expiresInSeconds: 3,
scope: 'sco pe',
tokenType: 'bear',
idToken: 'id-token',
refreshToken: 'refresh-token',
},
},
{ ctx: 'ctx' } as unknown as AuthResolverContext,
);
expect(authHandler).toHaveBeenCalledWith(
{
fullProfile: { id: 'id' },
accessToken: 'token',
params: {
scope: 'sco pe',
id_token: 'id-token',
expires_in: 3,
token_type: 'bear',
},
},
{ ctx: 'ctx' },
);
});
});
@@ -1,46 +0,0 @@
/*
* Copyright 2023 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 {
OAuthAuthenticatorResult,
ProfileTransform,
} from '@backstage/plugin-auth-node';
import { AuthHandler } from '../../providers';
import { OAuthResult } from '../oauth';
import { PassportProfile } from '../passport/types';
/** @internal */
export function adaptLegacyOAuthHandler(
authHandler?: AuthHandler<OAuthResult>,
): ProfileTransform<OAuthAuthenticatorResult<PassportProfile>> | undefined {
return (
authHandler &&
(async (result, ctx) =>
authHandler(
{
fullProfile: result.fullProfile,
accessToken: result.session.accessToken,
params: {
scope: result.session.scope,
id_token: result.session.idToken,
token_type: result.session.tokenType,
expires_in: result.session.expiresInSeconds!,
},
},
ctx,
))
);
}
@@ -1,69 +0,0 @@
/*
* Copyright 2023 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 {
AuthResolverContext,
PassportProfile,
} from '@backstage/plugin-auth-node';
import { adaptLegacyOAuthSignInResolver } from './adaptLegacyOAuthSignInResolver';
describe('adaptLegacyOAuthSignInResolver', () => {
it('should pass through undefined', () => {
expect(adaptLegacyOAuthSignInResolver(undefined)).toBeUndefined();
});
it('should convert a legacy resolver to a new one', () => {
const legacyResolver = jest.fn();
const newResolver = adaptLegacyOAuthSignInResolver(legacyResolver);
newResolver?.(
{
profile: { email: 'em@i.l' },
result: {
fullProfile: { id: 'id' } as PassportProfile,
session: {
accessToken: 'token',
expiresInSeconds: 3,
scope: 'sco pe',
tokenType: 'bear',
idToken: 'id-token',
refreshToken: 'refresh-token',
},
},
},
{ ctx: 'ctx' } as unknown as AuthResolverContext,
);
expect(legacyResolver).toHaveBeenCalledWith(
{
profile: { email: 'em@i.l' },
result: {
fullProfile: { id: 'id' },
accessToken: 'token',
refreshToken: 'refresh-token',
params: {
scope: 'sco pe',
id_token: 'id-token',
expires_in: 3,
token_type: 'bear',
},
},
},
{ ctx: 'ctx' },
);
});
});
@@ -1,49 +0,0 @@
/*
* Copyright 2023 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 {
OAuthAuthenticatorResult,
PassportProfile,
SignInResolver,
} from '@backstage/plugin-auth-node';
import { OAuthResult } from '../oauth';
/** @internal */
export function adaptLegacyOAuthSignInResolver(
signInResolver?: SignInResolver<OAuthResult>,
): SignInResolver<OAuthAuthenticatorResult<PassportProfile>> | undefined {
return (
signInResolver &&
(async (input, ctx) =>
signInResolver(
{
profile: input.profile,
result: {
fullProfile: input.result.fullProfile,
accessToken: input.result.session.accessToken,
refreshToken: input.result.session.refreshToken,
params: {
scope: input.result.session.scope,
id_token: input.result.session.idToken,
token_type: input.result.session.tokenType,
expires_in: input.result.session.expiresInSeconds!,
},
},
},
ctx,
))
);
}
@@ -1,85 +0,0 @@
/*
* Copyright 2023 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 {
AuthResolverContext,
PassportProfile,
} from '@backstage/plugin-auth-node';
import { adaptOAuthSignInResolverToLegacy } from './adaptOAuthSignInResolverToLegacy';
describe('adaptOAuthSignInResolverToLegacy', () => {
it('should pass through an empty object', () => {
const legacyResolvers = adaptOAuthSignInResolverToLegacy({});
expect(legacyResolvers).toEqual({});
// @ts-expect-error
legacyResolvers.missing?.();
});
it('should adapt a collection of sign-in resolvers', () => {
const resolverA = jest.fn();
const resolverB = jest.fn();
const legacyResolvers = adaptOAuthSignInResolverToLegacy({
resolverA,
resolverB,
});
const legacyResolverA = legacyResolvers.resolverA();
legacyResolverA(
{
profile: { email: 'em@i.l' },
result: {
fullProfile: { id: 'id' } as PassportProfile,
accessToken: 'token',
refreshToken: 'refresh-token',
params: {
scope: 'sco pe',
id_token: 'id-token',
expires_in: 3,
token_type: 'bear',
},
},
},
{ ctx: 'ctx' } as unknown as AuthResolverContext,
);
expect(resolverA).toHaveBeenCalledWith(
{
profile: { email: 'em@i.l' },
result: {
fullProfile: { id: 'id' } as PassportProfile,
session: {
accessToken: 'token',
expiresInSeconds: 3,
scope: 'sco pe',
tokenType: 'bear',
idToken: 'id-token',
refreshToken: 'refresh-token',
},
},
},
{ ctx: 'ctx' },
);
expect(resolverB).not.toHaveBeenCalled();
legacyResolvers.resolverB()(
{ profile: {}, result: { params: {} } } as any,
{} as any,
);
expect(resolverB).toHaveBeenCalled();
});
});
@@ -1,55 +0,0 @@
/*
* Copyright 2023 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 {
OAuthAuthenticatorResult,
PassportProfile,
SignInResolver,
} from '@backstage/plugin-auth-node';
import { OAuthResult } from '../oauth';
/** @internal */
export function adaptOAuthSignInResolverToLegacy<
TKeys extends string,
>(resolvers: {
[key in TKeys]: SignInResolver<OAuthAuthenticatorResult<PassportProfile>>;
}): { [key in TKeys]: () => SignInResolver<OAuthResult> } {
const legacyResolvers = {} as {
[key in TKeys]: () => SignInResolver<OAuthResult>;
};
for (const name of Object.keys(resolvers) as TKeys[]) {
const resolver = resolvers[name];
legacyResolvers[name] = () => async (input, ctx) =>
resolver(
{
profile: input.profile,
result: {
fullProfile: input.result.fullProfile,
session: {
accessToken: input.result.accessToken,
expiresInSeconds: input.result.params.expires_in,
scope: input.result.params.scope,
idToken: input.result.params.id_token,
tokenType: input.result.params.token_type ?? 'bearer',
refreshToken: input.result.refreshToken,
},
},
},
ctx,
);
}
return legacyResolvers;
}
@@ -1,19 +0,0 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { adaptLegacyOAuthHandler } from './adaptLegacyOAuthHandler';
export { adaptLegacyOAuthSignInResolver } from './adaptLegacyOAuthSignInResolver';
export { adaptOAuthSignInResolverToLegacy } from './adaptOAuthSignInResolverToLegacy';
@@ -1,549 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import express from 'express';
import { THOUSAND_DAYS_MS, TEN_MINUTES_MS, OAuthAdapter } from './OAuthAdapter';
import { encodeState } from './helpers';
import { OAuthHandlers, OAuthLogoutRequest } from './types';
import { CookieConfigurer, OAuthState } from '@backstage/plugin-auth-node';
const mockResponseData = {
providerInfo: {
accessToken: 'ACCESS_TOKEN',
token: 'ID_TOKEN',
expiresInSeconds: 10,
scope: 'email',
},
profile: {
email: 'foo@bar.com',
},
backstageIdentity: {
token:
'eyblob.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iLCJlbnQiOlsidXNlcjpkZWZhdWx0L2ppbW15bWFya3VtIl19.eyblob',
},
};
describe('OAuthAdapter', () => {
beforeEach(() => {
jest.clearAllMocks();
});
class MyAuthProvider implements OAuthHandlers {
async start() {
return {
url: '/url',
status: 301,
};
}
async handler() {
return {
response: mockResponseData,
refreshToken: 'token',
};
}
async refresh() {
return {
response: mockResponseData,
refreshToken: 'token',
};
}
async logout(_: OAuthLogoutRequest) {}
}
const providerInstance = new MyAuthProvider();
const mockCookieConfig: ReturnType<CookieConfigurer> = {
domain: 'domain.org',
path: '/auth/test-provider',
secure: false,
};
const mockCookieConfigurer = jest.fn().mockReturnValue(mockCookieConfig);
const oAuthProviderOptions = {
providerId: 'test-provider',
appOrigin: 'http://localhost:3000',
baseUrl: 'http://domain.org/auth',
cookieConfigurer: mockCookieConfigurer,
tokenIssuer: {
issueToken: async () => 'my-id-token',
listPublicKeys: async () => ({ keys: [] }),
},
isOriginAllowed: () => false,
callbackUrl: 'http://domain.org/auth/test-provider/handler/frame',
};
const defaultState = { nonce: 'nonce', env: 'development' };
const createEncodedQueryMockRequest = (state: any) => {
return {
cookies: {
'test-provider-nonce': 'nonce',
},
query: {
state: encodeState(state),
},
} as unknown as express.Request;
};
const mockResponse = {
cookie: jest.fn().mockReturnThis(),
end: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
statusCode: jest.fn().mockReturnThis(),
redirect: jest.fn().mockReturnThis(),
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
} as unknown as express.Response;
const mockStartRequest = {
query: {
scope: 'user',
env: 'development',
},
} as unknown as express.Request;
const expectedStartAuthCookieData = {
httpOnly: true,
path: '/auth/test-provider/handler',
maxAge: TEN_MINUTES_MS,
domain: 'domain.org',
sameSite: 'lax',
secure: false,
};
it('sets the correct headers in start', async () => {
const oauthProvider = new OAuthAdapter(
providerInstance,
oAuthProviderOptions,
);
await oauthProvider.start(mockStartRequest, mockResponse);
// nonce cookie checks
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledWith(
`${oAuthProviderOptions.providerId}-nonce`,
expect.any(String),
expect.objectContaining(expectedStartAuthCookieData),
);
expect(mockResponse.setHeader).toHaveBeenCalledTimes(2);
expect(mockResponse.setHeader).toHaveBeenCalledWith('Location', '/url');
expect(mockResponse.setHeader).toHaveBeenCalledWith('Content-Length', '0');
expect(mockResponse.statusCode).toEqual(301);
expect(mockResponse.end).toHaveBeenCalledTimes(1);
expect(mockResponse.redirect).not.toHaveBeenCalled();
});
const refreshCookieData = {
...expectedStartAuthCookieData,
path: '/auth/test-provider',
maxAge: THOUSAND_DAYS_MS,
};
it('sets the refresh cookie if refresh is enabled', async () => {
const oauthProvider = new OAuthAdapter(providerInstance, {
...oAuthProviderOptions,
isOriginAllowed: () => false,
});
const mockRequest = createEncodedQueryMockRequest(defaultState);
await oauthProvider.frameHandler(mockRequest, mockResponse);
expect(mockCookieConfigurer).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledWith(
expect.stringContaining('test-provider-refresh-token'),
expect.stringContaining('token'),
expect.objectContaining(refreshCookieData),
);
expect(mockResponse.redirect).not.toHaveBeenCalled();
});
it('sets the refresh cookie if refresh is enabled with redirect', async () => {
const oauthProvider = new OAuthAdapter(providerInstance, {
...oAuthProviderOptions,
isOriginAllowed: () => false,
});
const state = {
...defaultState,
redirectUrl: 'http://localhost:3000',
flow: 'redirect',
};
const mockRequest = createEncodedQueryMockRequest(state);
await oauthProvider.frameHandler(mockRequest, mockResponse);
expect(mockResponse.redirect).toHaveBeenCalledTimes(1);
});
it('persists scope through cookie if enabled', async () => {
const handlers = {
start: jest.fn(async (_req: { state: OAuthState }) => ({
url: '/url',
status: 301,
})),
handler: jest.fn(async () => ({ response: mockResponseData })),
refresh: jest.fn(async () => ({ response: mockResponseData })),
};
const oauthProvider = new OAuthAdapter(handlers, {
...oAuthProviderOptions,
persistScopes: true,
});
// First we test the /start request, making sure state is set
await oauthProvider.start(mockStartRequest, mockResponse);
expect(handlers.start).toHaveBeenCalledTimes(1);
expect(handlers.start).toHaveBeenCalledWith({
...mockStartRequest,
scope: 'user',
state: {
nonce: expect.any(String),
env: 'development',
scope: 'user',
},
});
// Then test the /handler, making sure the granted scope cookie is set
const providedState = handlers.start.mock.calls[0][0].state;
const mockHandleReq = {
cookies: {
'test-provider-nonce': providedState.nonce,
},
query: {
state: encodeState(providedState),
},
} as unknown as express.Request;
const mockHandleRes = {
cookie: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
end: jest.fn().mockReturnThis(),
redirect: jest.fn().mockReturnThis(),
} as unknown as express.Response;
await oauthProvider.frameHandler(mockHandleReq, mockHandleRes);
expect(mockHandleRes.cookie).toHaveBeenCalledTimes(1);
expect(mockHandleRes.cookie).toHaveBeenCalledWith(
'test-provider-granted-scope',
'user',
expect.objectContaining(refreshCookieData),
);
expect(mockResponse.redirect).not.toHaveBeenCalled();
// Then make sure scopes are forwarded correctly during refresh
const mockRefreshReq = {
query: { scope: 'ignore-me' },
cookies: {
'test-provider-granted-scope': 'user',
'test-provider-refresh-token': 'refresh-token',
},
header: jest.fn().mockReturnValue('XMLHttpRequest'),
} as unknown as express.Request;
const mockRefreshRes = {
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
redirect: jest.fn().mockReturnThis(),
} as unknown as express.Response;
await oauthProvider.refresh(mockRefreshReq, mockRefreshRes);
expect(handlers.refresh).toHaveBeenCalledTimes(1);
expect(handlers.refresh).toHaveBeenCalledWith(
expect.objectContaining({
scope: 'user',
refreshToken: 'refresh-token',
}),
);
expect(mockRefreshRes.redirect).not.toHaveBeenCalled();
});
const mockRequestWithHeader = {
header: () => 'XMLHttpRequest',
cookies: {
'test-provider-refresh-token': 'token',
},
query: {},
get: jest.fn(),
} as unknown as express.Request;
it('removes refresh cookie and calls logout handler when logging out', async () => {
const logoutSpy = jest.spyOn(providerInstance, 'logout');
const oauthProvider = new OAuthAdapter(providerInstance, {
...oAuthProviderOptions,
isOriginAllowed: () => false,
});
await oauthProvider.logout(mockRequestWithHeader, mockResponse);
expect(mockRequestWithHeader.get).toHaveBeenCalledTimes(1);
expect(logoutSpy).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledWith(
expect.stringContaining('test-provider-refresh-token'),
'',
expect.objectContaining({ path: '/auth/test-provider' }),
);
expect(mockResponse.end).toHaveBeenCalledTimes(1);
expect(mockResponse.redirect).not.toHaveBeenCalled();
});
it('gets new access-token when refreshing', async () => {
const oauthProvider = new OAuthAdapter(providerInstance, {
...oAuthProviderOptions,
isOriginAllowed: () => false,
});
await oauthProvider.refresh(mockRequestWithHeader, mockResponse);
expect(mockResponse.json).toHaveBeenCalledTimes(1);
expect(mockResponse.json).toHaveBeenCalledWith({
...mockResponseData,
backstageIdentity: {
token: mockResponseData.backstageIdentity.token,
identity: {
type: 'user',
userEntityRef: 'user:default/jimmymarkum',
ownershipEntityRefs: ['user:default/jimmymarkum'],
},
},
});
expect(mockResponse.redirect).not.toHaveBeenCalled();
});
it('sets new access-token when old cookie exists', async () => {
const oauthProvider = new OAuthAdapter(providerInstance, {
...oAuthProviderOptions,
isOriginAllowed: () => false,
});
const mockRequest = {
...mockRequestWithHeader,
cookies: {
'test-provider-refresh-token': 'old-token',
},
} as unknown as express.Request;
await oauthProvider.refresh(mockRequest, mockResponse);
expect(mockRequest.get).toHaveBeenCalledTimes(1);
expect(mockCookieConfigurer).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledWith(
'test-provider-refresh-token',
'token',
expect.objectContaining(refreshCookieData),
);
expect(mockResponse.redirect).not.toHaveBeenCalled();
});
it('sets the correct nonce cookie configuration', async () => {
const config = {
baseUrl: 'http://domain.org/auth',
appUrl: 'http://domain.org',
isOriginAllowed: () => false,
};
const oauthProvider = OAuthAdapter.fromConfig(config, providerInstance, {
...oAuthProviderOptions,
});
await oauthProvider.start(mockStartRequest, mockResponse);
expect(mockCookieConfigurer).not.toHaveBeenCalled();
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledWith(
`${oAuthProviderOptions.providerId}-nonce`,
expect.any(String),
expect.objectContaining(expectedStartAuthCookieData),
);
expect(mockResponse.redirect).not.toHaveBeenCalled();
});
const config = {
baseUrl: 'http://domain.org/auth',
appUrl: 'http://domain.org',
isOriginAllowed: () => false,
};
const mockStartRequestWithOrigin = {
query: {
scope: 'user',
env: 'development',
origin: 'http://other.domain',
},
} as unknown as express.Request;
it('sets the correct nonce cookie configuration using origin from request', async () => {
const oauthProvider = OAuthAdapter.fromConfig(config, providerInstance, {
...oAuthProviderOptions,
callbackUrl: 'https://domain.org/auth/test-provider/handler/frame',
});
await oauthProvider.start(mockStartRequestWithOrigin, mockResponse);
expect(mockCookieConfigurer).not.toHaveBeenCalled();
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledWith(
`${oAuthProviderOptions.providerId}-nonce`,
expect.any(String),
expect.objectContaining({
...expectedStartAuthCookieData,
secure: true,
sameSite: 'none',
}),
);
expect(mockResponse.redirect).not.toHaveBeenCalled();
});
const secureCookieData = {
...refreshCookieData,
secure: true,
sameSite: 'lax',
maxAge: THOUSAND_DAYS_MS,
};
it('sets the correct cookie configuration using an secure callbackUrl', async () => {
const oauthProvider = OAuthAdapter.fromConfig(config, providerInstance, {
...oAuthProviderOptions,
callbackUrl: 'https://domain.org/auth/test-provider/handler/frame',
});
const mockRequest = createEncodedQueryMockRequest(defaultState);
await oauthProvider.frameHandler(mockRequest, mockResponse);
expect(mockCookieConfigurer).not.toHaveBeenCalled();
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledWith(
expect.stringContaining('test-provider-refresh-token'),
expect.stringContaining('token'),
expect.objectContaining(secureCookieData),
);
expect(mockResponse.redirect).not.toHaveBeenCalled();
});
const secureSameSiteNoneCookieData = {
...secureCookieData,
sameSite: 'none',
};
it('sets the correct cookie configuration when on different domains and secure', async () => {
const oauthProvider = OAuthAdapter.fromConfig(config, providerInstance, {
...oAuthProviderOptions,
callbackUrl: 'https://authdomain.org/auth/test-provider/handler/frame',
});
const mockRequest = createEncodedQueryMockRequest(defaultState);
await oauthProvider.frameHandler(mockRequest, mockResponse);
expect(mockCookieConfigurer).not.toHaveBeenCalled();
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledWith(
expect.stringContaining('test-provider-refresh-token'),
expect.stringContaining('token'),
expect.objectContaining({
...secureSameSiteNoneCookieData,
domain: 'authdomain.org',
}),
);
expect(mockResponse.redirect).not.toHaveBeenCalled();
});
const configOriginAllowed = {
...config,
isOriginAllowed: () => true,
};
it('sets the correct cookie configuration using origin from state', async () => {
const oauthProvider = OAuthAdapter.fromConfig(
configOriginAllowed,
providerInstance,
{
...oAuthProviderOptions,
callbackUrl: 'https://domain.org/auth/test-provider/handler/frame',
},
);
const mockRequest = createEncodedQueryMockRequest({
...defaultState,
origin: 'http://other.domain',
});
await oauthProvider.frameHandler(mockRequest, mockResponse);
expect(mockCookieConfigurer).not.toHaveBeenCalled();
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledWith(
expect.stringContaining('test-provider-refresh-token'),
expect.stringContaining('token'),
expect.objectContaining(secureSameSiteNoneCookieData),
);
expect(mockResponse.redirect).not.toHaveBeenCalled();
});
const mockRequestWithGetMockReturn = {
header: () => 'XMLHttpRequest',
cookies: {
'test-provider-refresh-token': 'old-token',
},
query: {},
get: jest.fn().mockReturnValue('http://other.domain'),
} as unknown as express.Request;
it('sets the correct cookie configuration using origin from header', async () => {
const oauthProvider = OAuthAdapter.fromConfig(config, providerInstance, {
...oAuthProviderOptions,
callbackUrl: 'https://domain.org/auth/test-provider/handler/frame',
});
await oauthProvider.refresh(mockRequestWithGetMockReturn, mockResponse);
expect(mockRequestWithGetMockReturn.get).toHaveBeenCalledTimes(1);
expect(mockCookieConfigurer).not.toHaveBeenCalled();
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledWith(
'test-provider-refresh-token',
'token',
expect.objectContaining(secureSameSiteNoneCookieData),
);
expect(mockResponse.redirect).not.toHaveBeenCalled();
});
it('executed a response redirect when flow query string is set to "redirect"', async () => {
const handlers = {
start: jest.fn(async (_req: { state: OAuthState }) => ({
url: '/url',
status: 301,
})),
handler: jest.fn(async () => ({ response: mockResponseData })),
refresh: jest.fn(async () => ({ response: mockResponseData })),
};
const configWithNoPopupEnabled = {
...configOriginAllowed,
};
const oauthProvider = OAuthAdapter.fromConfig(
configWithNoPopupEnabled,
handlers,
{
...oAuthProviderOptions,
callbackUrl: 'https://domain.org/auth/test-provider/handler/frame',
},
);
const state = {
...defaultState,
origin: 'http://other.domain',
redirectUrl: 'http://domain.org',
flow: 'redirect',
};
const mockRequest = {
...createEncodedQueryMockRequest(state),
get: jest.fn().mockReturnValue('http://other.domain'),
} as unknown as express.Request;
await oauthProvider.frameHandler(mockRequest, mockResponse);
expect(mockRequest.get).not.toHaveBeenCalled();
expect(mockCookieConfigurer).not.toHaveBeenCalled();
expect(mockResponse.cookie).not.toHaveBeenCalled();
expect(mockResponse.redirect).toHaveBeenCalledTimes(1);
expect(mockResponse.redirect).toHaveBeenCalledWith('http://domain.org');
});
});

Some files were not shown because too many files have changed in this diff Show More