mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 00:49:03 +00:00
Website: Add VPP metadata proxy (#37997)
For https://github.com/fleetdm/fleet/issues/37261 Changes: - Added a new database model: `FleetInstanceUsingVpp` - Added `/api/vpp/v1/register`: An API endpoint that validates provided Fleet license keys, creates a database record for the proxy registration, and returns a generated secret used to authenticate requests to the other VPP proxy endpoint - Added `/api/vpp/v1/metadata/:storeRegion`: An API endpoint that forwards requests to the `https://api.ent.apple.com/v1/catalog/${storeRegion}/stoken-authenticated-apps` Apple API with a token generated using Fleet's Apple developer credentials. --------- Co-authored-by: Ian Littman <iansltx@gmail.com>
This commit is contained in:
parent
4ab5874e25
commit
b3bd4686a3
8 changed files with 262 additions and 1 deletions
1
website/.eslintrc
vendored
1
website/.eslintrc
vendored
|
|
@ -54,6 +54,7 @@
|
|||
"MicrosoftComplianceTenant": true,
|
||||
"AndroidEnterprise": true,
|
||||
"RenderProofOfValue": true,
|
||||
"FleetInstanceUsingVpp": true
|
||||
// …and any others.
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
},
|
||||
|
|
|
|||
125
website/api/controllers/vpp-proxy/get-vpp-app-metadata.js
vendored
Normal file
125
website/api/controllers/vpp-proxy/get-vpp-app-metadata.js
vendored
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
module.exports = {
|
||||
|
||||
|
||||
friendlyName: 'Get vpp app metadata',
|
||||
|
||||
|
||||
description: 'Proxies authenticated requests from Fleet instances to the Apple App store API.',
|
||||
|
||||
|
||||
inputs: {
|
||||
storeRegion: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The App store region that a proxied request will be sent to.',
|
||||
},
|
||||
|
||||
platform: {
|
||||
type: 'string',
|
||||
description: 'The platform the specified app(s) runs on',
|
||||
},
|
||||
|
||||
additionalPlatforms: {
|
||||
type: 'string',
|
||||
description: 'A comma separated list of platforms that are included in the proxied request.'
|
||||
},
|
||||
|
||||
ids: {
|
||||
type: 'string',
|
||||
description: 'A comma separated list of IDs of app store apps to include in the response.'
|
||||
},
|
||||
|
||||
extend: {
|
||||
type: {},
|
||||
description: 'An object containing the name of additional attributes to include in the API response.'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
exits: {
|
||||
success: {
|
||||
description: 'App metadata was sent to the Fleet server',
|
||||
outputType: {},
|
||||
},
|
||||
missingAuthHeader: {
|
||||
description: 'This request is missing an authorization header.',
|
||||
responseType: 'unauthorized'
|
||||
},
|
||||
invalidFleetServerSecret: {
|
||||
description: 'Invalid authentication token.',
|
||||
responseType: 'unauthorized',
|
||||
},
|
||||
missingVppToken: {
|
||||
description: 'This request is missing a VPP app token',
|
||||
responseType: 'badRequest',
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
fn: async function ({storeRegion, ids, platform, additionalPlatforms, extend}) {
|
||||
|
||||
// Validate the provided fleetServerSecret
|
||||
let authHeader = this.req.get('authorization');
|
||||
let fleetServerSecret;
|
||||
|
||||
if (authHeader && authHeader.startsWith('Bearer')) {
|
||||
fleetServerSecret = authHeader.replace('Bearer', '').trim();
|
||||
} else {
|
||||
throw 'missingAuthHeader';
|
||||
}
|
||||
|
||||
let thisFleetInstance = await FleetInstanceUsingVpp.findOne({
|
||||
fleetServerSecret: fleetServerSecret
|
||||
});
|
||||
|
||||
if(!thisFleetInstance) {
|
||||
throw 'invalidFleetServerSecret';
|
||||
}
|
||||
|
||||
let cookieHeader = this.req.get('cookie');
|
||||
if(!cookieHeader) {
|
||||
// If no cookie header was included return a missingVppToken (badRequest) response to the Fleet instance.
|
||||
throw 'missingVppToken';
|
||||
}
|
||||
let nowAt = Date.now();
|
||||
let nowAtInSeconds = Math.floor(nowAt / 1000);
|
||||
|
||||
let expiresAtInSeconds = nowAtInSeconds + 60;
|
||||
|
||||
let tokenForThisRequest = require('jsonwebtoken').sign(
|
||||
{
|
||||
iss: sails.config.custom.vppProxyTokenTeamId,
|
||||
exp: expiresAtInSeconds,
|
||||
iat: nowAtInSeconds,
|
||||
},
|
||||
sails.config.custom.vppProxyTokenPrivateKey,
|
||||
{
|
||||
algorithm: 'ES256',
|
||||
keyid: sails.config.custom.vppProxyTokenKeyId,
|
||||
}
|
||||
);
|
||||
|
||||
let responseFromAppleApi = await sails.helpers.http.get.with({
|
||||
url: `https://api.ent.apple.com/v1/catalog/${storeRegion}/stoken-authenticated-apps`,
|
||||
data: {
|
||||
ids,
|
||||
platform,
|
||||
additionalPlatforms,
|
||||
extend,
|
||||
},
|
||||
headers: {
|
||||
'Authorization': `Bearer ${tokenForThisRequest}`,
|
||||
'Cookie': `${cookieHeader}`,
|
||||
}
|
||||
})
|
||||
.tolerate((err)=>{
|
||||
sails.log.warn(`When a Fleet instance sent a proxied request to the Apple App Store API, an error occured. Full error: ${require('util').inspect(err)}`);
|
||||
return err;
|
||||
});
|
||||
|
||||
return responseFromAppleApi;
|
||||
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
83
website/api/controllers/vpp-proxy/register-one-fleet-instance-using-vpp.js
vendored
Normal file
83
website/api/controllers/vpp-proxy/register-one-fleet-instance-using-vpp.js
vendored
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
module.exports = {
|
||||
|
||||
|
||||
friendlyName: 'Register one Fleet instance for using VPP',
|
||||
|
||||
|
||||
description: 'Creates a registration for a Fleet instance in the website\'s database and returns a generated secret to authenticate future requests.',
|
||||
|
||||
|
||||
|
||||
inputs: {
|
||||
fleetServerUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
exits: {
|
||||
success: {
|
||||
description: 'A Fleet instance\'s VPP proxy registration was successfully submitted.',
|
||||
outputType: {
|
||||
fleetServerSecret: 'string',
|
||||
},
|
||||
},
|
||||
couldNotVerifyLicense: {
|
||||
description: 'The Fleet license key could not be verified.',
|
||||
responseType: 'unauthorized',
|
||||
},
|
||||
invalidFleetServerUrl: {
|
||||
description: 'The provided Fleet server URL does not appear to be a URL.',
|
||||
responseType: 'badRequest',
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
fn: async function ({fleetServerUrl}) {
|
||||
|
||||
// Get the Fleet license key that was sent in the Authorization header as a bearer token.
|
||||
let authHeader = this.req.get('authorization');
|
||||
let fleetLicenseKey;
|
||||
|
||||
if (authHeader && authHeader.startsWith('Bearer')) {
|
||||
fleetLicenseKey = authHeader.replace('Bearer', '').trim();
|
||||
} else {
|
||||
throw 'couldNotVerifyLicense';
|
||||
}
|
||||
|
||||
// Validate provided fleetLicenseKey
|
||||
try {
|
||||
require('jsonwebtoken').verify(
|
||||
fleetLicenseKey,
|
||||
sails.config.custom.licenseKeyGeneratorPublicKey,
|
||||
{ algorithm: 'ES256' }
|
||||
);
|
||||
} catch(unusedErr) {
|
||||
// If there is an error parsing the provided fleetLicenseKey, return a couldNotVerifyLicense response.
|
||||
throw 'couldNotVerifyLicense';
|
||||
}
|
||||
|
||||
// validate Fleet server URL
|
||||
try {
|
||||
new URL(fleetServerUrl);
|
||||
} catch(unusedErr) {
|
||||
throw 'invalidFleetServerUrl';
|
||||
}
|
||||
|
||||
|
||||
// Generate a new FleetServerSecret for this Fleet instance.
|
||||
let fleetServerSecret = sails.helpers.strings.random.with({len: 30});
|
||||
|
||||
// Create a new database record for this Fleet instance.
|
||||
await FleetInstanceUsingVpp.create({fleetInstanceUrl: fleetServerUrl, fleetServerSecret});
|
||||
|
||||
// Return the generated fleetServerSecret
|
||||
return {
|
||||
fleetServerSecret,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
39
website/api/models/FleetInstanceUsingVpp.js
vendored
Normal file
39
website/api/models/FleetInstanceUsingVpp.js
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* InstanceUsingVpp.js// TODO: better name.
|
||||
*
|
||||
* @description :: A model definition represents a database table/collection.
|
||||
* @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
|
||||
attributes: {
|
||||
|
||||
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
|
||||
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
|
||||
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
|
||||
fleetInstanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The URL of a Fleet instance that is using the fleet website VPP proxy.'
|
||||
},
|
||||
|
||||
fleetServerSecret: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'A generated secret used to autheticate & identify requests from a particular Fleet instance.'
|
||||
},
|
||||
|
||||
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
|
||||
// ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝
|
||||
|
||||
|
||||
// ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
|
||||
// ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
|
||||
// ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
|
||||
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
3
website/assets/.eslintrc
vendored
3
website/assets/.eslintrc
vendored
|
|
@ -67,7 +67,8 @@
|
|||
"Platform": false,
|
||||
"AdCampaign": false,
|
||||
"MicrosoftComplianceTenant": false,
|
||||
"AndroidEnterprise": false
|
||||
"AndroidEnterprise": false,
|
||||
"FleetInstanceUsingVpp": false
|
||||
// ...and any other backend globals (e.g. `"Organization": false`)
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
}
|
||||
|
|
|
|||
4
website/config/custom.js
vendored
4
website/config/custom.js
vendored
|
|
@ -488,5 +488,9 @@ module.exports.custom = {
|
|||
// androidEnterpriseServiceAccountEmailAddress: '…',
|
||||
// androidEnterpriseServiceAccountPrivateKey: '…',
|
||||
|
||||
// VPP proxy
|
||||
// vppProxyTokenTeamId: '',
|
||||
// vppProxyTokenKeyId: '',
|
||||
// vppProxyTokenPrivateKey: '',
|
||||
|
||||
};
|
||||
|
|
|
|||
1
website/config/policies.js
vendored
1
website/config/policies.js
vendored
|
|
@ -82,4 +82,5 @@ module.exports.policies = {
|
|||
'microsoft-proxy/view-turn-on-mdm': true,
|
||||
'view-okta-conditional-access-error': true,
|
||||
'view-fast-track': true,
|
||||
'vpp-proxy/*': true,
|
||||
};
|
||||
|
|
|
|||
7
website/config/routes.js
vendored
7
website/config/routes.js
vendored
|
|
@ -1272,6 +1272,13 @@ module.exports.routes = {
|
|||
'GET /api/v1/microsoft-compliance-partner/device/message': { action: 'microsoft-proxy/get-one-compliance-status-result', },
|
||||
'GET /api/v1/microsoft-compliance-partner/adminconsent': { action: 'microsoft-proxy/receive-redirect-from-microsoft', },
|
||||
|
||||
//
|
||||
// ╦ ╦╔═╗╔═╗ ╔╦╗╔═╗╔╦╗╔═╗╔╦╗╔═╗╔╦╗╔═╗ ╔═╗╦═╗╔═╗═╗ ╦╦ ╦ ╔═╗╔╗╔╔╦╗╔═╗╔═╗╦╔╗╔╔╦╗╔═╗
|
||||
// ╚╗╔╝╠═╝╠═╝ ║║║║╣ ║ ╠═╣ ║║╠═╣ ║ ╠═╣ ╠═╝╠╦╝║ ║╔╩╦╝╚╦╝ ║╣ ║║║ ║║╠═╝║ ║║║║║ ║ ╚═╗
|
||||
// ╚╝ ╩ ╩ ╩ ╩╚═╝ ╩ ╩ ╩═╩╝╩ ╩ ╩ ╩ ╩ ╩ ╩╚═╚═╝╩ ╚═ ╩ ╚═╝╝╚╝═╩╝╩ ╚═╝╩╝╚╝ ╩ ╚═╝
|
||||
'POST /api/vpp/v1/register': { action: 'vpp-proxy/register-one-fleet-instance-using-vpp', csrf: false },
|
||||
'GET /api/vpp/v1/metadata/:storeRegion': { action: 'vpp-proxy/get-vpp-app-metadata' },
|
||||
|
||||
// Well known resources https://datatracker.ietf.org/doc/html/rfc8615
|
||||
// =============================================================================================================
|
||||
//
|
||||
|
|
|
|||
Loading…
Reference in a new issue