mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-20 06:07:57 +00:00
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:
parent
50aab3ba78
commit
104a8006fb
@ -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 = (() => {
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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
41
src/migrations/M00007.js
Normal 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'));
|
||||
}
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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');
|
||||
|
Loading…
Reference in New Issue
Block a user