mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-01-18 18:56:24 +00:00
fix: Scoped keys breaking livepush with existing cloud images on the device
Closes: #1512 Change-type: patch Signed-off-by: Rich Bayliss <rich@balena.io>
This commit is contained in:
parent
53e7412f75
commit
02aeb4fc1c
@ -510,25 +510,22 @@ export function createV2Api(router: Router) {
|
||||
overallDownloadProgress = downloadProgressTotal / downloads;
|
||||
}
|
||||
|
||||
if (_.uniq(appIds).length > 1) {
|
||||
// We can't accurately return the commit value each app without changing
|
||||
// the shape of the data, and instead we'd like users to use the new v3
|
||||
// endpoints, which will come with multiapp
|
||||
// If we're going to return information about more than one app, error out
|
||||
return res.status(405).json({
|
||||
status: 'failed',
|
||||
message: `Cannot use /v2/ endpoints with a key that is scoped to multiple applications`,
|
||||
});
|
||||
}
|
||||
|
||||
const commit = await commitStore.getCommitForApp(appIds[0]);
|
||||
// This endpoint does not support multi-app but the device might be running multiple apps
|
||||
// We must return information for only 1 application so use the first one in the list
|
||||
const appId = appIds[0];
|
||||
// Get the commit for this application
|
||||
const commit = await commitStore.getCommitForApp(appId);
|
||||
// Filter containers by this application
|
||||
const appContainers = containerStates.filter((c) => c.appId === appId);
|
||||
// Filter images by this application
|
||||
const appImages = imagesStates.filter((i) => i.appId === appId);
|
||||
|
||||
return res.status(200).send({
|
||||
status: 'success',
|
||||
appState: pending ? 'applying' : 'applied',
|
||||
overallDownloadProgress,
|
||||
containers: containerStates,
|
||||
images: imagesStates,
|
||||
containers: appContainers,
|
||||
images: appImages,
|
||||
release: commit,
|
||||
});
|
||||
});
|
||||
|
@ -121,7 +121,7 @@ export const authMiddleware: AuthorizedRequestHandler = async (
|
||||
req.auth = {
|
||||
apiKey,
|
||||
scopes: [],
|
||||
isScoped: () => false,
|
||||
isScoped: (resources) => isScoped(resources, req.auth.scopes),
|
||||
};
|
||||
|
||||
try {
|
||||
@ -134,6 +134,7 @@ export const authMiddleware: AuthorizedRequestHandler = async (
|
||||
|
||||
// no need to authenticate, shortcut
|
||||
if (!needsAuth) {
|
||||
// Allow requests that do not need auth to be scoped for all applications
|
||||
req.auth.isScoped = () => true;
|
||||
return next();
|
||||
}
|
||||
@ -146,10 +147,6 @@ export const authMiddleware: AuthorizedRequestHandler = async (
|
||||
if (scopes != null) {
|
||||
// keep the scopes for later incase they're desired
|
||||
req.auth.scopes.push(...scopes);
|
||||
|
||||
// which resources are scoped...
|
||||
req.auth.isScoped = (resources) => isScoped(resources, req.auth.scopes);
|
||||
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
@ -13,17 +13,52 @@ import * as applicationManager from '../src/compose/application-manager';
|
||||
import { InstancedAppState } from '../src/types/state';
|
||||
|
||||
import * as serviceManager from '../src/compose/service-manager';
|
||||
import * as images from '../src/compose/images';
|
||||
|
||||
import * as apiKeys from '../src/lib/api-keys';
|
||||
import * as db from '../src/db';
|
||||
import * as config from '../src/config';
|
||||
import { Service } from '../src/compose/service';
|
||||
import { Image } from '../src/compose/images';
|
||||
|
||||
const mockedOptions = {
|
||||
listenPort: 54321,
|
||||
timeout: 30000,
|
||||
};
|
||||
|
||||
const mockService = (overrides?: Partial<Service>) => {
|
||||
return {
|
||||
...{
|
||||
appId: 1658654,
|
||||
status: 'Running',
|
||||
serviceName: 'main',
|
||||
imageId: 2885946,
|
||||
serviceId: 640681,
|
||||
containerId:
|
||||
'f93d386599d1b36e71272d46ad69770cff333842db04e2e4c64dda7b54da07c6',
|
||||
createdAt: '2020-11-13T20:29:44.143Z',
|
||||
},
|
||||
...overrides,
|
||||
} as Service;
|
||||
};
|
||||
|
||||
const mockImage = (overrides?: Partial<Image>) => {
|
||||
return {
|
||||
...{
|
||||
name:
|
||||
'registry2.balena-cloud.com/v2/e2bf6410ffc30850e96f5071cdd1dca8@sha256:e2e87a8139b8fc14510095b210ad652d7d5badcc64fdc686cbf749d399fba15e',
|
||||
appId: 1658654,
|
||||
serviceName: 'main',
|
||||
imageId: 2885946,
|
||||
dockerImageId:
|
||||
'sha256:4502983d72e2c72bc292effad1b15b49576da3801356f47fd275ba274d409c1a',
|
||||
status: 'Downloaded',
|
||||
downloadProgress: null,
|
||||
},
|
||||
...overrides,
|
||||
} as Image;
|
||||
};
|
||||
|
||||
describe('SupervisorAPI', () => {
|
||||
let api: SupervisorAPI;
|
||||
let healthCheckStubs: SinonStub[];
|
||||
@ -286,6 +321,19 @@ describe('SupervisorAPI', () => {
|
||||
});
|
||||
|
||||
describe('V2 endpoints', () => {
|
||||
let serviceManagerMock: SinonStub;
|
||||
let imagesMock: SinonStub;
|
||||
|
||||
before(async () => {
|
||||
serviceManagerMock = stub(serviceManager, 'getAll').resolves([]);
|
||||
imagesMock = stub(images, 'getStatus').resolves([]);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
serviceManagerMock.restore();
|
||||
imagesMock.restore();
|
||||
});
|
||||
|
||||
describe('GET /v2/device/vpn', () => {
|
||||
it('returns information about VPN connection', async () => {
|
||||
await request
|
||||
@ -359,63 +407,135 @@ describe('SupervisorAPI', () => {
|
||||
});
|
||||
|
||||
describe('GET /v2/state/status', () => {
|
||||
let serviceManagerMock: SinonStub;
|
||||
|
||||
const mockService = (
|
||||
appId: number,
|
||||
serviceId: number,
|
||||
serviceName: string,
|
||||
) => {
|
||||
return {
|
||||
appId,
|
||||
status: 'Running',
|
||||
serviceName,
|
||||
imageId: appId,
|
||||
serviceId,
|
||||
containerId: Math.random()
|
||||
.toString(36)
|
||||
.replace(/[^a-z]+/g, '')
|
||||
.substr(0, 16),
|
||||
createdAt: new Date(),
|
||||
} as Service;
|
||||
};
|
||||
|
||||
before(async () => {
|
||||
await config.set({ localMode: true });
|
||||
serviceManagerMock = stub(serviceManager, 'getAll').resolves([]);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await config.set({ localMode: false });
|
||||
serviceManagerMock.restore();
|
||||
});
|
||||
|
||||
it('should succeed in LocalMode with a single application', async () => {
|
||||
serviceManagerMock.resolves([mockService(1, 1, 'main')]);
|
||||
|
||||
const { body } = await request
|
||||
.get('/v2/state/status')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.have.property('status').which.equals('success');
|
||||
expect(body).to.have.property('appState').which.equals('applied');
|
||||
expect(body)
|
||||
.to.have.property('containers')
|
||||
.which.is.an('array')
|
||||
.with.lengthOf(1);
|
||||
});
|
||||
|
||||
it('should error in LocalMode with multiple applications', async () => {
|
||||
serviceManagerMock.resolves([
|
||||
mockService(1, 1, 'main'),
|
||||
mockService(2, 2, 'extra'),
|
||||
]);
|
||||
|
||||
it('should return scoped application', async () => {
|
||||
// Create scoped key for application
|
||||
const appScopedKey = await apiKeys.generateScopedKey(1658654, 640681);
|
||||
// Setup device conditions
|
||||
serviceManagerMock.resolves([mockService({ appId: 1658654 })]);
|
||||
imagesMock.resolves([mockImage({ appId: 1658654 })]);
|
||||
// Make request and evaluate response
|
||||
await request
|
||||
.get('/v2/state/status')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(405);
|
||||
.set('Authorization', `Bearer ${appScopedKey}`)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(
|
||||
sampleResponses.V2.GET['/state/status?desc=single_application']
|
||||
.statusCode,
|
||||
)
|
||||
.then((response) => {
|
||||
expect(response.body).to.deep.equal(
|
||||
sampleResponses.V2.GET['/state/status?desc=single_application']
|
||||
.body,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
// Setup device conditions
|
||||
serviceManagerMock.resolves([mockService({ appId: 1658654 })]);
|
||||
imagesMock.resolves([mockImage({ appId: 1658654 })]);
|
||||
// Make request and evaluate response
|
||||
await request
|
||||
.get('/v2/state/status')
|
||||
.set('Accept', 'application/json')
|
||||
.set('Authorization', `Bearer ${appScopedKey}`)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(
|
||||
sampleResponses.V2.GET['/state/status?desc=no_applications']
|
||||
.statusCode,
|
||||
)
|
||||
.then((response) => {
|
||||
expect(response.body).to.deep.equal(
|
||||
sampleResponses.V2.GET['/state/status?desc=no_applications'].body,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return success when device has no applications', async () => {
|
||||
// Create scoped key for any application
|
||||
const appScopedKey = await apiKeys.generateScopedKey(1658654, 1658654);
|
||||
// Setup device conditions
|
||||
serviceManagerMock.resolves([]);
|
||||
imagesMock.resolves([]);
|
||||
// Make request and evaluate response
|
||||
await request
|
||||
.get('/v2/state/status')
|
||||
.set('Accept', 'application/json')
|
||||
.set('Authorization', `Bearer ${appScopedKey}`)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(
|
||||
sampleResponses.V2.GET['/state/status?desc=no_applications']
|
||||
.statusCode,
|
||||
)
|
||||
.then((response) => {
|
||||
expect(response.body).to.deep.equal(
|
||||
sampleResponses.V2.GET['/state/status?desc=no_applications'].body,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
// Setup device conditions
|
||||
serviceManagerMock.resolves([
|
||||
mockService({ appId: 1658654 }),
|
||||
mockService({ appId: 222222 }),
|
||||
]);
|
||||
imagesMock.resolves([
|
||||
mockImage({ appId: 1658654 }),
|
||||
mockImage({ appId: 222222 }),
|
||||
]);
|
||||
// Make request and evaluate response
|
||||
await request
|
||||
.get('/v2/state/status')
|
||||
.set('Accept', 'application/json')
|
||||
.set('Authorization', `Bearer ${appScopedKey}`)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(
|
||||
sampleResponses.V2.GET['/state/status?desc=single_application']
|
||||
.statusCode,
|
||||
)
|
||||
.then((response) => {
|
||||
expect(response.body).to.deep.equal(
|
||||
sampleResponses.V2.GET['/state/status?desc=single_application']
|
||||
.body,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should only return 1 application when in LOCAL MODE (no auth)', async () => {
|
||||
// Activate localmode
|
||||
await config.set({ localMode: true });
|
||||
// Setup device conditions
|
||||
serviceManagerMock.resolves([
|
||||
mockService({ appId: 1658654 }),
|
||||
mockService({ appId: 222222 }),
|
||||
]);
|
||||
imagesMock.resolves([
|
||||
mockImage({ appId: 1658654 }),
|
||||
mockImage({ appId: 222222 }),
|
||||
]);
|
||||
// Make request and evaluate response
|
||||
await request
|
||||
.get('/v2/state/status')
|
||||
.set('Accept', 'application/json')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(
|
||||
sampleResponses.V2.GET['/state/status?desc=single_application']
|
||||
.statusCode,
|
||||
)
|
||||
.then((response) => {
|
||||
expect(response.body).to.deep.equal(
|
||||
sampleResponses.V2.GET['/state/status?desc=single_application']
|
||||
.body,
|
||||
);
|
||||
});
|
||||
// Deactivate localmode
|
||||
await config.set({ localMode: false });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -77,8 +77,48 @@
|
||||
"status": "failed",
|
||||
"message": "Invalid application ID: 123invalid"
|
||||
}
|
||||
},
|
||||
"/state/status?desc=single_application": {
|
||||
"statusCode": 200,
|
||||
"body": {
|
||||
"status": "success",
|
||||
"appState": "applied",
|
||||
"overallDownloadProgress": null,
|
||||
"containers": [
|
||||
{
|
||||
"appId": 1658654,
|
||||
"status": "Running",
|
||||
"serviceName": "main",
|
||||
"imageId": 2885946,
|
||||
"serviceId": 640681,
|
||||
"containerId": "f93d386599d1b36e71272d46ad69770cff333842db04e2e4c64dda7b54da07c6",
|
||||
"createdAt": "2020-11-13T20:29:44.143Z"
|
||||
}
|
||||
],
|
||||
"images": [
|
||||
{
|
||||
"name": "registry2.balena-cloud.com/v2/e2bf6410ffc30850e96f5071cdd1dca8@sha256:e2e87a8139b8fc14510095b210ad652d7d5badcc64fdc686cbf749d399fba15e",
|
||||
"appId": 1658654,
|
||||
"serviceName": "main",
|
||||
"imageId": 2885946,
|
||||
"dockerImageId": "sha256:4502983d72e2c72bc292effad1b15b49576da3801356f47fd275ba274d409c1a",
|
||||
"status": "Downloaded",
|
||||
"downloadProgress": null
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/state/status?desc=no_applications": {
|
||||
"statusCode": 200,
|
||||
"body": {
|
||||
"status": "success",
|
||||
"appState": "applied",
|
||||
"overallDownloadProgress": null,
|
||||
"containers": [],
|
||||
"images": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"POST": {}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user