Update apiSecret table to id services by name

It adds a migration replacing the serviceId column by serviceName and
populates serviceNames from services in the target state.
This commit is contained in:
Felipe Lalanne 2021-07-27 16:51:05 -04:00
parent 50aab3ba78
commit 104a8006fb
7 changed files with 77 additions and 39 deletions

View File

@ -373,7 +373,7 @@ export async function addFeaturesFromLabels(
// create a app/service specific API secret
const apiSecret = await apiKeys.generateScopedKey(
service.appId,
service.serviceId,
service.serviceName,
);
const host = (() => {

View File

@ -121,13 +121,6 @@ export async function doPurge(appId, force) {
});
}
export function serviceAction(action, serviceId, current, target, options) {
if (options == null) {
options = {};
}
return { action, serviceId, current, target, options };
}
/**
* This doesn't truly return an InstancedDeviceState, but it's close enough to mostly work where it's used
*

View File

@ -15,7 +15,7 @@ export class KeyNotFoundError extends Error {}
interface DbApiSecret {
id: number;
appId: number;
serviceId: number;
serviceName: string;
scopes: string;
key: string;
}
@ -199,17 +199,17 @@ export async function getScopesForKey(key: string): Promise<Scope[] | null> {
export async function generateScopedKey(
appId: number,
serviceId: number,
serviceName: string,
options?: Partial<GenerateKeyOptions>,
): Promise<string> {
await initialized;
return await generateKey(appId, serviceId, options);
return await generateKey(appId, serviceName, options);
}
export async function generateCloudKey(
force: boolean = false,
): Promise<string> {
cloudApiKey = await generateKey(0, 0, {
cloudApiKey = await generateKey(0, null, {
force,
scopes: [{ type: 'global' }],
});
@ -223,15 +223,15 @@ export async function refreshKey(key: string): Promise<string> {
throw new KeyNotFoundError();
}
const { appId, serviceId, scopes } = apiKey;
const { appId, serviceName, scopes } = apiKey;
// if this is a cloud key that is being refreshed
if (appId === 0 && serviceId === 0) {
if (appId === 0 && serviceName === null) {
return await generateCloudKey(true);
}
// generate a new key, expiring the old one...
const newKey = await generateScopedKey(appId, serviceId, {
const newKey = await generateScopedKey(appId, serviceName, {
force: true,
scopes: deserialiseScopes(scopes),
});
@ -244,15 +244,15 @@ export async function refreshKey(key: string): Promise<string> {
* A cached lookup of the database key
*/
const getApiKeyForService = memoizee(
async (appId: number, serviceId: number): Promise<DbApiSecret[]> => {
async (appId: number, serviceName: string | null): Promise<DbApiSecret[]> => {
await db.initialized;
return await db.models('apiSecret').where({ appId, serviceId }).select();
return await db.models('apiSecret').where({ appId, serviceName }).select();
},
{
promise: true,
maxAge: 60000, // 1 minute
normalizer: ([appId, serviceId]) => `${appId}-${serviceId}`,
normalizer: ([appId, serviceName]) => `${appId}-${serviceName}`,
},
);
@ -276,12 +276,12 @@ const getApiKeyByKey = memoizee(
* All key generate logic should come though this method. It handles cache clearing.
*
* @param appId
* @param serviceId
* @param serviceName
* @param options
*/
async function generateKey(
appId: number,
serviceId: number,
serviceName: string | null,
options?: Partial<GenerateKeyOptions>,
): Promise<string> {
// set default options
@ -292,13 +292,13 @@ async function generateKey(
};
// grab the existing API key info
const secrets = await getApiKeyForService(appId, serviceId);
const secrets = await getApiKeyForService(appId, serviceName);
// if we need a new key
if (secrets.length === 0 || force) {
// are forcing a new key?
if (force) {
await db.models('apiSecret').where({ appId, serviceId }).del();
await db.models('apiSecret').where({ appId, serviceName }).del();
}
// remove the cached lookup for the key
@ -308,10 +308,10 @@ async function generateKey(
}
// remove the cached value for this lookup
getApiKeyForService.clear(appId, serviceId);
getApiKeyForService.clear(appId, serviceName);
// return a new API key
return await createNewKey(appId, serviceId, scopes);
return await createNewKey(appId, serviceName, scopes);
}
// grab the current secret and scopes
@ -333,21 +333,25 @@ async function generateKey(
}
// forcibly get a new key...
return await generateKey(appId, serviceId, { ...options, force: true });
return await generateKey(appId, serviceName, { ...options, force: true });
}
/**
* Generates a new key value and inserts it into the DB.
*
* @param appId
* @param serviceId
* @param serviceName
* @param scopes
*/
async function createNewKey(appId: number, serviceId: number, scopes: Scope[]) {
async function createNewKey(
appId: number,
serviceName: string | null,
scopes: Scope[],
) {
const key = generateUniqueKey();
await db.models('apiSecret').insert({
appId,
serviceId,
serviceName,
key,
scopes: serialiseScopes(scopes),
});

41
src/migrations/M00007.js Normal file
View File

@ -0,0 +1,41 @@
export async function up(knex) {
// Add serviceName to apiSecret schema
await knex.schema.table('apiSecret', (table) => {
table.string('serviceName');
table.unique(['appId', 'serviceName']);
});
const targetServices = (await knex('app').select(['appId', 'services']))
.map(({ appId, services }) => ({
appId,
// Extract service name and id per app
services: JSON.parse(services).map(({ serviceId, serviceName }) => ({
serviceId,
serviceName,
})),
}))
.reduce(
// Convert to array of {appId, serviceId, serviceName}
(apps, { appId, services }) =>
apps.concat(services.map((svc) => ({ appId, ...svc }))),
[],
);
// Update all API secret entries so services can still access the API after
// the change
await Promise.all(
targetServices.map(({ appId, serviceId, serviceName }) =>
knex('apiSecret').update({ serviceName }).where({ appId, serviceId }),
),
);
// Update the table schema deleting the serviceId column
await knex.schema.table('apiSecret', (table) => {
table.dropUnique(['appId', 'serviceId']);
table.dropColumn('serviceId');
});
}
export function down() {
return Promise.reject(new Error('Not Implemented'));
}

View File

@ -64,7 +64,7 @@ describe('SupervisorAPI', () => {
describe('API Key Scope', () => {
it('should generate a key which is scoped for a single application', async () => {
// single app scoped key...
const appScopedKey = await apiKeys.generateScopedKey(1, 1);
const appScopedKey = await apiKeys.generateScopedKey(1, 'main');
await request
.get('/v2/applications/1/state')
@ -74,7 +74,7 @@ describe('SupervisorAPI', () => {
});
it('should generate a key which is scoped for multiple applications', async () => {
// multi-app scoped key...
const multiAppScopedKey = await apiKeys.generateScopedKey(1, 2, {
const multiAppScopedKey = await apiKeys.generateScopedKey(1, 'other', {
scopes: [1, 2].map((appId) => {
return { type: 'app', appId };
}),
@ -135,7 +135,7 @@ describe('SupervisorAPI', () => {
});
it('should regenerate a key and invalidate the old one', async () => {
// single app scoped key...
const appScopedKey = await apiKeys.generateScopedKey(1, 1);
const appScopedKey = await apiKeys.generateScopedKey(1, 'main');
await request
.get('/v2/applications/1/state')

View File

@ -1232,7 +1232,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
// resolve to true for any isScoped check
const scopedKey = await apiKeys.generateScopedKey(
2,
containers[0].serviceId,
containers[0].serviceName,
);
await request

View File

@ -142,7 +142,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
describe('Scoped API Keys', () => {
it('returns 409 because app is out of scope of the key', async () => {
const apiKey = await apiKeys.generateScopedKey(3, 1);
const apiKey = await apiKeys.generateScopedKey(3, 'main');
await request
.get('/v2/applications/2/state')
.set('Accept', 'application/json')
@ -164,7 +164,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
it('should return scoped application', async () => {
// Create scoped key for application
const appScopedKey = await apiKeys.generateScopedKey(1658654, 640681);
const appScopedKey = await apiKeys.generateScopedKey(1658654, 'main');
// Setup device conditions
serviceManagerMock.resolves([mockedAPI.mockService({ appId: 1658654 })]);
imagesMock.resolves([mockedAPI.mockImage({ appId: 1658654 })]);
@ -188,7 +188,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
it('should return no application info due to lack of scope', async () => {
// Create scoped key for wrong application
const appScopedKey = await apiKeys.generateScopedKey(1, 1);
const appScopedKey = await apiKeys.generateScopedKey(1, 'main');
// Setup device conditions
serviceManagerMock.resolves([mockedAPI.mockService({ appId: 1658654 })]);
imagesMock.resolves([mockedAPI.mockImage({ appId: 1658654 })]);
@ -211,7 +211,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
it('should return success when device has no applications', async () => {
// Create scoped key for any application
const appScopedKey = await apiKeys.generateScopedKey(1658654, 1658654);
const appScopedKey = await apiKeys.generateScopedKey(1658654, 'main');
// Setup device conditions
serviceManagerMock.resolves([]);
imagesMock.resolves([]);
@ -234,7 +234,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
it('should only return 1 application when N > 1 applications on device', async () => {
// Create scoped key for application
const appScopedKey = await apiKeys.generateScopedKey(1658654, 640681);
const appScopedKey = await apiKeys.generateScopedKey(1658654, 'main');
// Setup device conditions
serviceManagerMock.resolves([
mockedAPI.mockService({ appId: 1658654 }),
@ -330,7 +330,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
before(async () => {
// Create scoped key for application
appScopedKey = await apiKeys.generateScopedKey(1658654, 640681);
appScopedKey = await apiKeys.generateScopedKey(1658654, 'main');
// Mock target state cache
targetStateCacheMock = stub(targetStateCache, 'getTargetApp');
@ -439,7 +439,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
before(async () => {
// Create scoped key for application
appScopedKey = await apiKeys.generateScopedKey(1658654, 640681);
appScopedKey = await apiKeys.generateScopedKey(1658654, 'main');
// Mock target state cache
targetStateCacheMock = stub(targetStateCache, 'getTargetApp');