Reintroduce noop token manager and refactor ServerPermissionClient

Signed-off-by: Joon Park <joonp@spotify.com>
This commit is contained in:
Joon Park
2021-12-13 14:10:10 +00:00
parent ea96ce2449
commit 24dce3ca43
12 changed files with 232 additions and 26 deletions
@@ -0,0 +1,118 @@
/*
* 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 { ServerPermissionClient } from '.';
import {
DiscoveryApi,
Permission,
Identified,
AuthorizeRequest,
AuthorizeResult,
} from '@backstage/plugin-permission-common';
import { ConfigReader } from '@backstage/config';
import { getVoidLogger, ServerTokenManager } from '@backstage/backend-common';
import { setupServer } from 'msw/node';
import { RestContext, rest } from 'msw';
const server = setupServer();
const mockAuthorizeHandler = jest.fn((req, res, { json }: RestContext) => {
const responses = req.body.map((r: Identified<AuthorizeRequest>) => ({
id: r.id,
result: AuthorizeResult.ALLOW,
}));
return res(json(responses));
});
const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base';
const discoveryApi: DiscoveryApi = {
async getBaseUrl() {
return mockBaseUrl;
},
};
const testPermission: Permission = {
name: 'test.permission',
attributes: {},
resourceType: 'test-resource',
};
const config = new ConfigReader({
permission: { enabled: true },
backend: { auth: { keys: [{ secret: 'a-secret-key' }] } },
});
const logger = getVoidLogger();
describe('ServerPermissionClient', () => {
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterAll(() => server.close());
beforeEach(() => {
server.use(rest.post(`${mockBaseUrl}/authorize`, mockAuthorizeHandler));
});
afterEach(() => server.resetHandlers());
it('should bypass authorization if permissions are disabled', async () => {
const client = new ServerPermissionClient({
discoveryApi,
configApi: new ConfigReader({}),
serverTokenManager: ServerTokenManager.noop(),
});
await client.authorize([{ permission: testPermission }]);
expect(mockAuthorizeHandler).not.toHaveBeenCalled();
});
it('should bypass authorization if permissions are enabled and request has valid server token', async () => {
const tokenManager = ServerTokenManager.fromConfig(config, { logger });
const client = new ServerPermissionClient({
discoveryApi,
configApi: config,
serverTokenManager: tokenManager,
});
await client.authorize([{ permission: testPermission }], {
token: (await tokenManager.getToken()).token,
});
expect(mockAuthorizeHandler).not.toHaveBeenCalled();
});
it('should authorize normally if permissions are enabled and request does not have valid server token', async () => {
const tokenManager = ServerTokenManager.fromConfig(config, { logger });
const client = new ServerPermissionClient({
discoveryApi,
configApi: config,
serverTokenManager: tokenManager,
});
await client.authorize([{ permission: testPermission }], {
token: 'a-user-token',
});
expect(mockAuthorizeHandler).toHaveBeenCalled();
});
it('should error if permissions are enabled but a no-op token manager is configured', async () => {
expect(
() =>
new ServerPermissionClient({
discoveryApi,
configApi: config,
serverTokenManager: ServerTokenManager.noop(),
}),
).toThrowError(
'You must configure at least one key in backend.auth.keys if permissions are enabled.',
);
});
});
@@ -14,13 +14,10 @@
* limitations under the License.
*/
import { ServerTokenManager } from '@backstage/backend-common';
import { TokenManager, ServerTokenManager } from '@backstage/backend-common';
import { Config } from '@backstage/config';
import {
AuthorizeRequest,
AuthorizeRequestOptions,
AuthorizeResponse,
AuthorizeResult,
DiscoveryApi,
PermissionClient,
} from '@backstage/plugin-permission-common';
@@ -31,26 +28,36 @@ import {
* @public
*/
export class ServerPermissionClient extends PermissionClient {
private readonly serverTokenManager: ServerTokenManager;
private readonly serverTokenManager: TokenManager;
constructor(options: {
discoveryApi: DiscoveryApi;
configApi: Config;
serverTokenManager: ServerTokenManager;
serverTokenManager: TokenManager;
}) {
const { discoveryApi, configApi, serverTokenManager } = options;
super({ discoveryApi, configApi });
if (this.enabled && !(serverTokenManager instanceof ServerTokenManager)) {
throw new Error(
'You must configure at least one key in backend.auth.keys if permissions are enabled.',
);
}
this.serverTokenManager = serverTokenManager;
}
async authorize(
requests: AuthorizeRequest[],
options?: AuthorizeRequestOptions,
): Promise<AuthorizeResponse[]> {
if (await this.isValidServerToken(options?.token)) {
return requests.map(_ => ({ result: AuthorizeResult.ALLOW }));
async shouldBypass(options?: AuthorizeRequestOptions): Promise<boolean> {
// Call super first in order to check if permissions are enabled before
// validating the server token. That way when permissions are disabled, the
// noop token manager can be used without fouling up the logic inside the
// ServerPermissionClient, because the code path won't be reached.
if (await super.shouldBypass(options)) {
return true;
}
return super.authorize(requests, options);
if (await this.isValidServerToken(options?.token)) {
return true;
}
return false;
}
private async isValidServerToken(