diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index 52378fef37..d5985d1172 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -73,6 +73,7 @@ "minimist": "^1.2.5", "morgan": "^1.10.0", "node-forge": "^1.3.1", + "path-to-regexp": "^6.2.1", "selfsigned": "^2.0.0", "stoppable": "^1.1.0", "winston": "^3.2.1", diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.ts b/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.ts new file mode 100644 index 0000000000..c502dc25e5 --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.ts @@ -0,0 +1,89 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + HttpAuthService, + HttpRouterServiceAuthPolicy, +} from '@backstage/backend-plugin-api'; +import { RequestHandler } from 'express'; +import { pathToRegexp } from 'path-to-regexp'; + +export function createPathPolicyPredicate(policyPath: string) { + if (policyPath === '/' || policyPath === '*') { + return () => true; + } + + const pathRegex = pathToRegexp(policyPath, undefined, { + end: false, + }); + + return (path: string): boolean => { + return pathRegex.test(path); + }; +} + +export function createCredentialsBarrier(options: { + httpAuth: HttpAuthService; +}): { + middleware: RequestHandler; + addAuthPolicy: (policy: HttpRouterServiceAuthPolicy) => void; +} { + const { httpAuth } = options; + + const unauthenticatedPredicates = new Array<(path: string) => boolean>(); + const cookiePredicates = new Array<(path: string) => boolean>(); + + const middleware: RequestHandler = async (req, _, next) => { + const allowsUnauthenticated = unauthenticatedPredicates.some(predicate => + predicate(req.path), + ); + + if (allowsUnauthenticated) { + next(); + return; + } + + const allowsCookie = cookiePredicates.some(predicate => + predicate(req.path), + ); + + if (allowsCookie) { + // don't we need a user-cookie allow type here? + await httpAuth.credentials(req, { + allow: ['user-cookie', 'user', 'service'], + }); + next(); + return; + } + + await httpAuth.credentials(req, { + allow: ['user', 'service'], + }); + next(); + }; + + const addAuthPolicy = (policy: HttpRouterServiceAuthPolicy) => { + if (policy.allow === 'unauthenticated') { + unauthenticatedPredicates.push(createPathPolicyPredicate(policy.path)); + } else if (policy.allow === 'user-cookie') { + cookiePredicates.push(createPathPolicyPredicate(policy.path)); + } + + throw new Error('Invalid auth policy'); + }; + + return { middleware, addAuthPolicy }; +} diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index d49eefd8f2..302a3d37d4 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -299,10 +299,20 @@ export interface HttpAuthService { // @public (undocumented) export interface HttpRouterService { + // (undocumented) + addAuthPolicy(policy: HttpRouterServiceAuthPolicy): void; // (undocumented) use(handler: Handler): void; } +// @public (undocumented) +export interface HttpRouterServiceAuthPolicy { + // (undocumented) + allow: 'unauthenticated' | 'user-cookie'; + // (undocumented) + path: string; +} + // @public (undocumented) export interface IdentityService extends IdentityApi {} diff --git a/packages/backend-plugin-api/src/services/definitions/HttpRouterService.ts b/packages/backend-plugin-api/src/services/definitions/HttpRouterService.ts index 3c45ef1da1..695b337754 100644 --- a/packages/backend-plugin-api/src/services/definitions/HttpRouterService.ts +++ b/packages/backend-plugin-api/src/services/definitions/HttpRouterService.ts @@ -16,9 +16,17 @@ import { Handler } from 'express'; +/** @public */ +export interface HttpRouterServiceAuthPolicy { + path: string; + allow: 'unauthenticated' | 'user-cookie'; +} + /** * @public */ export interface HttpRouterService { use(handler: Handler): void; + + addAuthPolicy(policy: HttpRouterServiceAuthPolicy): void; } diff --git a/packages/backend-plugin-api/src/services/definitions/index.ts b/packages/backend-plugin-api/src/services/definitions/index.ts index 294bed1eb1..420c722a4d 100644 --- a/packages/backend-plugin-api/src/services/definitions/index.ts +++ b/packages/backend-plugin-api/src/services/definitions/index.ts @@ -29,7 +29,10 @@ export type { export type { RootConfigService } from './RootConfigService'; export type { DatabaseService } from './DatabaseService'; export type { DiscoveryService } from './DiscoveryService'; -export type { HttpRouterService } from './HttpRouterService'; +export type { + HttpRouterService, + HttpRouterServiceAuthPolicy, +} from './HttpRouterService'; export type { HttpAuthService, BackstageCredentialTypes, diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index bd684f7560..78f59b53c4 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -182,6 +182,7 @@ export namespace mockServices { export const factory = httpRouterServiceFactory; export const mock = simpleMock(coreServices.httpRouter, () => ({ use: jest.fn(), + addAuthPolicy: jest.fn(), })); } export namespace rootHttpRouter { diff --git a/yarn.lock b/yarn.lock index 202aa50d88..6d841a09db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3258,6 +3258,7 @@ __metadata: minimist: ^1.2.5 morgan: ^1.10.0 node-forge: ^1.3.1 + path-to-regexp: ^6.2.1 selfsigned: ^2.0.0 stoppable: ^1.1.0 supertest: ^6.1.3 @@ -37508,10 +37509,10 @@ __metadata: languageName: node linkType: hard -"path-to-regexp@npm:^6.2.0": - version: 6.2.0 - resolution: "path-to-regexp@npm:6.2.0" - checksum: a6aca74d2d6e2e7594d812f653cf85e9cb5054d3a8d80f099722a44ef6ad22639b02078e5ea83d11db16321c3e4359e3f1ab0274fa78dad0754a6e53f630b0fc +"path-to-regexp@npm:^6.2.0, path-to-regexp@npm:^6.2.1": + version: 6.2.1 + resolution: "path-to-regexp@npm:6.2.1" + checksum: f0227af8284ea13300f4293ba111e3635142f976d4197f14d5ad1f124aebd9118783dd2e5f1fe16f7273743cc3dbeddfb7493f237bb27c10fdae07020cc9b698 languageName: node linkType: hard