From a8c7b0da6a7c073a7769d5db418ec389ade3cca0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 4 Mar 2024 18:31:03 +0100 Subject: [PATCH] big improvements Signed-off-by: Patrik Oldsberg --- .../app/src/index-public-experimental.tsx | 41 ++++++--- packages/cli/src/lib/bundler/bundle.ts | 1 - packages/cli/src/lib/bundler/server.ts | 1 - plugins/app-backend/package.json | 1 + plugins/app-backend/src/service/appPlugin.ts | 8 +- plugins/app-backend/src/service/router.ts | 86 ++++++++++++++++++- yarn.lock | 1 + 7 files changed, 122 insertions(+), 17 deletions(-) diff --git a/packages/app/src/index-public-experimental.tsx b/packages/app/src/index-public-experimental.tsx index 9e5f89b4c2..59768a2dd1 100644 --- a/packages/app/src/index-public-experimental.tsx +++ b/packages/app/src/index-public-experimental.tsx @@ -22,26 +22,18 @@ import { SignInPage, } from '@backstage/core-components'; import React from 'react'; +import useAsync from 'react-use/lib/useAsync'; import ReactDOM from 'react-dom/client'; import { providers } from '../src/identityProviders'; import { configApiRef, createApiFactory, discoveryApiRef, + identityApiRef, useApi, } from '@backstage/core-plugin-api'; import { AuthProxyDiscoveryApi } from '../src/AuthProxyDiscoveryApi'; -// TODO(Rugvip): make this available via some util, or maybe Utility API? -function readBasePath(configApi: typeof configApiRef.T) { - let { pathname } = new URL( - configApi.getOptionalString('app.baseUrl') ?? '/', - 'http://sample.dev', // baseUrl can be specified as just a path - ); - pathname = pathname.replace(/\/*$/, ''); - return pathname; -} - const app = createApp({ apis: [ createApiFactory({ @@ -65,8 +57,33 @@ const app = createApp({ }); function RedirectToRoot() { - window.location.pathname = readBasePath(useApi(configApiRef)); - return
; + const identityApi = useApi(identityApiRef); + + const { value, loading, error } = useAsync(async () => { + const { token } = await identityApi.getCredentials(); + if (!token) { + throw new Error('Expected Backstage token in sign-in response'); + } + return token; + }, [identityApi]); + + if (loading) { + return null; + } else if (error) { + return <>An error occurred: {error}; + } + + return ( +
form?.submit()} + action={window.location.href} + method="POST" + > + + + +
+ ); } const App = app.createRoot( diff --git a/packages/cli/src/lib/bundler/bundle.ts b/packages/cli/src/lib/bundler/bundle.ts index 98ef2017b9..06249774fd 100644 --- a/packages/cli/src/lib/bundler/bundle.ts +++ b/packages/cli/src/lib/bundler/bundle.ts @@ -73,7 +73,6 @@ export async function buildBundle(options: BuildOptions) { configs.push( await createConfig(publicPaths, { ...commonConfigOptions, - publicSubPath: '/public', }), ); } diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index ea0e6e8b94..2fe72cd471 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -220,7 +220,6 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be config, await createConfig(publicPaths, { ...commonConfigOptions, - publicSubPath: '/public', }), ]) : webpack(config); diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index e775c0d3b2..3dc38bbcff 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -49,6 +49,7 @@ "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", "@backstage/config-loader": "workspace:^", + "@backstage/errors": "workspace:^", "@backstage/plugin-app-node": "workspace:^", "@backstage/types": "workspace:^", "@types/express": "^4.17.6", diff --git a/plugins/app-backend/src/service/appPlugin.ts b/plugins/app-backend/src/service/appPlugin.ts index 518b0dd307..b3ab9ed29a 100644 --- a/plugins/app-backend/src/service/appPlugin.ts +++ b/plugins/app-backend/src/service/appPlugin.ts @@ -65,8 +65,10 @@ export const appPlugin = createBackendPlugin({ config: coreServices.rootConfig, database: coreServices.database, httpRouter: coreServices.httpRouter, + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, }, - async init({ logger, config, database, httpRouter }) { + async init({ logger, config, database, httpRouter, auth, httpAuth }) { const appPackageName = config.getOptionalString('app.packageName') ?? 'app'; @@ -76,11 +78,15 @@ export const appPlugin = createBackendPlugin({ logger: winstonLogger, config, database, + auth, + httpAuth, appPackageName, staticFallbackHandler, schema, }); httpRouter.use(router); + + // Access control is handled within the router httpRouter.addAuthPolicy({ allow: 'unauthenticated', path: '/', diff --git a/plugins/app-backend/src/service/router.ts b/plugins/app-backend/src/service/router.ts index 01b98fcf3a..6bbfd2ad3c 100644 --- a/plugins/app-backend/src/service/router.ts +++ b/plugins/app-backend/src/service/router.ts @@ -38,6 +38,8 @@ import { CACHE_CONTROL_REVALIDATE_CACHE, } from '../lib/headers'; import { ConfigSchema } from '@backstage/config-loader'; +import { AuthService, HttpAuthService } from '@backstage/backend-plugin-api'; +import { AuthenticationError } from '@backstage/errors'; // express uses mime v1 while we only have types for mime v2 type Mime = { lookup(arg0: string): string }; @@ -46,6 +48,8 @@ type Mime = { lookup(arg0: string): string }; export interface RouterOptions { config: Config; logger: Logger; + auth?: AuthService; + httpAuth?: HttpAuthService; /** * If a database is provided it will be used to cache previously deployed static assets. @@ -99,7 +103,14 @@ export interface RouterOptions { export async function createRouter( options: RouterOptions, ): Promise { - const { config, logger, appPackageName, staticFallbackHandler } = options; + const { + config, + logger, + appPackageName, + staticFallbackHandler, + auth, + httpAuth, + } = options; const disableConfigInjection = options.disableConfigInjection ?? @@ -154,9 +165,80 @@ export async function createRouter( router.use(helmet.frameguard({ action: 'deny' })); + const publicDistDir = resolvePath(appDistDir, 'public'); + + const enablePublicEntryPoint = await fs.pathExists(publicDistDir); + + /* + TODO: + - Cookie refresh + - Backend endpoint, /.backstage/v1-cookie + - Frontend provider + - Remove issueUserCookie from HttpAuthService and move to auth-node + - Move RedirectToRoot to auth-react + - Document index-public-experimental & how to use + - Logout? How do we clear the cookie? + + */ + + if (enablePublicEntryPoint && auth && httpAuth) { + const publicRouter = Router(); + + // TODO + // publicRouter.use(createCookieAuthRouter({ httpAuth })) + + publicRouter.use(async (req, _res, next) => { + const credentials = await httpAuth.credentials(req, { + allow: ['user', 'service', 'none'], + allowLimitedAccess: true, + }); + + if (credentials.principal.type === 'none') { + next(); + } else { + next('router'); + } + }); + + publicRouter.post( + '*', + express.urlencoded({ extended: true }), + async (req, res, next) => { + if (req.body.type === 'sign-in') { + const credentials = await auth.authenticate(req.body.token); + + if (!auth.isPrincipal(credentials, 'user')) { + throw new AuthenticationError('Invalid token, not a user'); + } + + await httpAuth.issueUserCookie(res, { + credentials, + }); + + // Resume as if it was a GET request towards the outer protected router, serving index.html + req.method = 'GET'; + next('router'); + } else { + throw new Error('Invalid POST request to /'); + } + }, + ); + + publicRouter.use( + await createEntryPointRouter({ + logger: logger.child({ entry: 'public' }), + rootDir: publicDistDir, + assetStore: assetStore?.withNamespace('public'), + appConfigs, // TODO(Rugvip): We should not be including the full config here + }), + ); + + router.use(publicRouter); + } + router.use( await createEntryPointRouter({ - logger, + logger: logger.child({ entry: 'main' }), rootDir: appDistDir, assetStore, staticFallbackHandler, diff --git a/yarn.lock b/yarn.lock index 2df04a207e..606e10254f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4691,6 +4691,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/config-loader": "workspace:^" + "@backstage/errors": "workspace:^" "@backstage/plugin-app-node": "workspace:^" "@backstage/types": "workspace:^" "@types/express": ^4.17.6