search-backend: update to treat requests without credentials as unauthenticated

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-07-12 12:21:41 +02:00
parent 36f91e8956
commit 343f656eb1
4 changed files with 48 additions and 23 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search-backend': patch
---
The `AuthorizedSearchEngine` will now ignore the deprecated `token` option, and treat it as an unauthorized request. This will not have any effect in practice, since credentials are always provided by the router.
@@ -32,6 +32,7 @@ import {
decodePageCursor,
AuthorizedSearchEngine,
} from './AuthorizedSearchEngine';
import { mockCredentials, mockServices } from '@backstage/backend-test-utils';
describe('AuthorizedSearchEngine', () => {
const typeUsers = 'users';
@@ -117,10 +118,11 @@ describe('AuthorizedSearchEngine', () => {
searchEngine,
defaultTypes,
permissionEvaluator,
mockServices.auth(),
new ConfigReader({}),
);
const options = { token: 'token' };
const options = { credentials: mockCredentials.user() };
const allowAll: PermissionEvaluator['authorize'] &
PermissionEvaluator['authorizeConditional'] = async queries => {
@@ -149,7 +151,7 @@ describe('AuthorizedSearchEngine', () => {
types: ['one', 'two'],
filters,
},
{ token: 'token' },
options,
);
});
@@ -160,7 +162,18 @@ describe('AuthorizedSearchEngine', () => {
await authorizedSearchEngine.query({ term: '' }, options);
expect(mockedQuery).toHaveBeenCalledWith(
{ term: '', types: ['users', 'templates', 'services', 'groups'] },
{ token: 'token' },
options,
);
});
it('should fall back to none credentials if a token is provided', async () => {
mockedQuery.mockImplementation(async () => ({ results }));
mockedPermissionQuery.mockImplementation(allowAll);
await authorizedSearchEngine.query({ term: '' }, { token: 'token' });
expect(mockedQuery).toHaveBeenCalledWith(
{ term: '', types: ['users', 'templates', 'services', 'groups'] },
{ credentials: mockCredentials.none() },
);
});
@@ -184,7 +197,7 @@ describe('AuthorizedSearchEngine', () => {
);
expect(mockedQuery).toHaveBeenCalledWith(
{ term: '', types: ['users', 'templates'] },
{ token: 'token' },
options,
);
expect(mockedPermissionQuery).toHaveBeenCalledTimes(1);
expect(mockedPermissionQuery).toHaveBeenLastCalledWith(
@@ -192,7 +205,7 @@ describe('AuthorizedSearchEngine', () => {
{ permission: defaultTypes[typeUsers].visibilityPermission },
{ permission: defaultTypes[typeTemplates].visibilityPermission },
],
{ token: 'token' },
options,
);
});
@@ -218,7 +231,7 @@ describe('AuthorizedSearchEngine', () => {
expect(mockedQuery).toHaveBeenCalledWith(
{ term: '', types: ['templates', 'services', 'groups'] },
{ token: 'token' },
options,
);
expect(mockedPermissionQuery).toHaveBeenCalledTimes(1);
@@ -270,7 +283,7 @@ describe('AuthorizedSearchEngine', () => {
expect(mockedQuery).toHaveBeenCalledWith(
{ term: '', types: ['users'] },
{ token: 'token' },
options,
);
});
@@ -326,7 +339,7 @@ describe('AuthorizedSearchEngine', () => {
}),
},
],
{ token: 'token' },
options,
);
expect(mockedAuthorize).toHaveBeenCalledTimes(1);
expect(mockedAuthorize).toHaveBeenCalledWith(
@@ -338,7 +351,7 @@ describe('AuthorizedSearchEngine', () => {
resourceRef: 'template_doc_0',
},
],
{ token: 'token' },
options,
);
});
@@ -403,7 +416,7 @@ describe('AuthorizedSearchEngine', () => {
expect(mockedQuery).toHaveBeenNthCalledWith(
1,
{ term: '', types: ['users', 'templates', 'services'] },
{ token: 'token' },
options,
);
expect(mockedQuery).toHaveBeenNthCalledWith(
2,
@@ -412,7 +425,7 @@ describe('AuthorizedSearchEngine', () => {
types: ['users', 'templates', 'services'],
pageCursor: 'MQ==',
},
{ token: 'token' },
options,
);
expect(mockedQuery).toHaveBeenNthCalledWith(
3,
@@ -421,7 +434,7 @@ describe('AuthorizedSearchEngine', () => {
types: ['users', 'templates', 'services'],
pageCursor: 'Mg==',
},
{ token: 'token' },
options,
);
const expectedResult = allDocuments
@@ -494,7 +507,7 @@ describe('AuthorizedSearchEngine', () => {
expect(mockedQuery).toHaveBeenNthCalledWith(
1,
{ term: '', types: ['users', 'templates', 'services', 'groups'] },
{ token: 'token' },
options,
);
expect(mockedQuery).toHaveBeenNthCalledWith(
2,
@@ -503,7 +516,7 @@ describe('AuthorizedSearchEngine', () => {
types: ['users', 'templates', 'services', 'groups'],
pageCursor: 'MQ==',
},
{ token: 'token' },
options,
);
expect(mockedQuery).toHaveBeenNthCalledWith(
3,
@@ -512,7 +525,7 @@ describe('AuthorizedSearchEngine', () => {
types: ['users', 'templates', 'services', 'groups'],
pageCursor: 'Mg==',
},
{ token: 'token' },
options,
);
const expectedResult = allDocuments
@@ -574,7 +587,7 @@ describe('AuthorizedSearchEngine', () => {
expect(mockedQuery).toHaveBeenNthCalledWith(
1,
{ term: '', types: ['users', 'templates', 'services'] },
{ token: 'token' },
options,
);
expect(mockedQuery).toHaveBeenNthCalledWith(
2,
@@ -583,7 +596,7 @@ describe('AuthorizedSearchEngine', () => {
types: ['users', 'templates', 'services'],
pageCursor: 'MQ==',
},
{ token: 'token' },
options,
);
expect(mockedQuery).toHaveBeenNthCalledWith(
3,
@@ -592,7 +605,7 @@ describe('AuthorizedSearchEngine', () => {
types: ['users', 'templates', 'services'],
pageCursor: 'Mg==',
},
{ token: 'token' },
options,
);
const expectedResults = servicesWithAuth
@@ -39,7 +39,7 @@ import {
import { Config } from '@backstage/config';
import { InputError } from '@backstage/errors';
import { Writable } from 'stream';
import { PermissionsService } from '@backstage/backend-plugin-api';
import { AuthService, PermissionsService } from '@backstage/backend-plugin-api';
export function decodePageCursor(pageCursor?: string): { page: number } {
if (!pageCursor) {
@@ -71,6 +71,7 @@ export class AuthorizedSearchEngine implements SearchEngine {
private readonly searchEngine: SearchEngine,
private readonly types: Record<string, DocumentTypeInfo>,
private readonly permissions: PermissionsService,
private readonly auth: AuthService,
config: Config,
) {
this.queryLatencyBudgetMs =
@@ -92,9 +93,14 @@ export class AuthorizedSearchEngine implements SearchEngine {
): Promise<IndexableResultSet> {
const queryStartTime = Date.now();
const compatOptions =
'credentials' in options
? options
: { credentials: await this.auth.getNoneCredentials() };
const conditionFetcher = new DataLoader(
(requests: readonly QueryPermissionRequest[]) =>
this.permissions.authorizeConditional(requests.slice(), options),
this.permissions.authorizeConditional(requests.slice(), compatOptions),
{
cacheKeyFn: ({ permission: { name } }) => name,
},
@@ -102,7 +108,7 @@ export class AuthorizedSearchEngine implements SearchEngine {
const authorizer = new DataLoader(
(requests: readonly AuthorizePermissionRequest[]) =>
this.permissions.authorize(requests.slice(), options),
this.permissions.authorize(requests.slice(), compatOptions),
{
// Serialize the permission name and resourceRef as
// a query string to avoid collisions from overlapping
@@ -159,7 +165,7 @@ export class AuthorizedSearchEngine implements SearchEngine {
if (!resultByResultFilteringRequired) {
return this.searchEngine.query(
{ ...query, types: authorizedTypes },
options,
compatOptions,
);
}
@@ -174,7 +180,7 @@ export class AuthorizedSearchEngine implements SearchEngine {
do {
const nextPage = await this.searchEngine.query(
{ ...query, types: authorizedTypes, pageCursor: nextPageCursor },
options,
compatOptions,
);
filteredResults = filteredResults.concat(
@@ -145,6 +145,7 @@ export async function createRouter(
inputEngine,
types,
permissionEvaluator,
auth,
config,
)
: inputEngine;