From 0ade43e7988eb0228b45edd57baee1103cc4e8e3 Mon Sep 17 00:00:00 2001 From: Carlo <1778532+cdcme@users.noreply.github.com> Date: Thu, 9 Oct 2025 08:17:48 -0400 Subject: [PATCH] Add proxy endpoints for Android (#34021) Fixes #34018. Adds endpoints for `GET`, `DELETE`, and `PATCH` operations for Android hosts. --- .../android-proxy/delete-android-device.js | 90 ++++++++++++++++++ .../android-proxy/get-android-device.js | 91 ++++++++++++++++++ .../android-proxy/modify-android-device.js | 92 +++++++++++++++++++ website/config/routes.js | 3 + 4 files changed, 276 insertions(+) create mode 100644 website/api/controllers/android-proxy/delete-android-device.js create mode 100644 website/api/controllers/android-proxy/get-android-device.js create mode 100644 website/api/controllers/android-proxy/modify-android-device.js diff --git a/website/api/controllers/android-proxy/delete-android-device.js b/website/api/controllers/android-proxy/delete-android-device.js new file mode 100644 index 0000000000..c32c38732a --- /dev/null +++ b/website/api/controllers/android-proxy/delete-android-device.js @@ -0,0 +1,90 @@ +module.exports = { + + + friendlyName: 'Delete android device', + + + description: 'Deletes a device of an Android enterprise', + + + inputs: { + androidEnterpriseId: { + type: 'string', + required: true, + }, + deviceId: { + type: 'string', + required: true, + }, + }, + + + exits: { + success: { description: 'The device of an Android enterprise was successfully deleted.' } + }, + + + fn: async function ({ androidEnterpriseId, deviceId}) { + + // Extract fleetServerSecret from the Authorization header + let authHeader = this.req.get('authorization'); + let fleetServerSecret; + + if (authHeader && authHeader.startsWith('Bearer')) { + fleetServerSecret = authHeader.replace('Bearer', '').trim(); + } else { + return this.res.unauthorized('Authorization header with Bearer token is required'); + } + + // Authenticate this request + let thisAndroidEnterprise = await AndroidEnterprise.findOne({ + androidEnterpriseId: androidEnterpriseId + }); + + // Return a 404 response if no records are found. + if (!thisAndroidEnterprise) { + return this.res.notFound(); + } + // Return an unauthorized response if the provided secret does not match. + if (thisAndroidEnterprise.fleetServerSecret !== fleetServerSecret) { + return this.res.unauthorized(); + } + + // Check the list of Android Enterprises managed by Fleet to see if this Android Enterprise is still managed. + let isEnterpriseManagedByFleet = await sails.helpers.androidProxy.getIsEnterpriseManagedByFleet(androidEnterpriseId); + // Return a 404 response if this Android enterprise is no longer managed by Fleet. + if(!isEnterpriseManagedByFleet) { + return this.res.notFound(); + } + + // Delete the device for this Android enterprise. + // Note: We're using sails.helpers.flow.build here to handle any errors that occur using google's node library. + await sails.helpers.flow.build(async () => { + let { google } = require('googleapis'); + let androidmanagement = google.androidmanagement('v1'); + let googleAuth = new google.auth.GoogleAuth({ + scopes: ['https://www.googleapis.com/auth/androidmanagement'], + credentials: { + client_email: sails.config.custom.androidEnterpriseServiceAccountEmailAddress,// eslint-disable-line camelcase + private_key: sails.config.custom.androidEnterpriseServiceAccountPrivateKey,// eslint-disable-line camelcase + }, + }); + // Acquire the google auth client, and bind it to all future calls + let authClient = await googleAuth.getClient(); + google.options({ auth: authClient }); + // [?]: https://googleapis.dev/nodejs/googleapis/latest/androidmanagement/classes/Resource$Enterprises$Devices.html#delete + await androidmanagement.enterprises.devices.delete({ + name: `enterprises/${androidEnterpriseId}/devices/${deviceId}`, + }); + }).intercept((err) => { + return new Error(`When attempting to delete a device for an Android enterprise (${androidEnterpriseId}), an error occurred. Error: ${err}`); + }); + + + // Return success response to the Fleet server. + return {}; + + } + + +}; diff --git a/website/api/controllers/android-proxy/get-android-device.js b/website/api/controllers/android-proxy/get-android-device.js new file mode 100644 index 0000000000..5479d31483 --- /dev/null +++ b/website/api/controllers/android-proxy/get-android-device.js @@ -0,0 +1,91 @@ +module.exports = { + + + friendlyName: 'Get android device', + + + description: 'Gets a device of an Android enterprise', + + + inputs: { + androidEnterpriseId: { + type: 'string', + required: true, + }, + deviceId: { + type: 'string', + required: true, + }, + }, + + + exits: { + success: { description: 'The device of an Android enterprise was successfully retrieved.' } + }, + + + fn: async function ({ androidEnterpriseId, deviceId}) { + + // Extract fleetServerSecret from the Authorization header + let authHeader = this.req.get('authorization'); + let fleetServerSecret; + + if (authHeader && authHeader.startsWith('Bearer')) { + fleetServerSecret = authHeader.replace('Bearer', '').trim(); + } else { + return this.res.unauthorized('Authorization header with Bearer token is required'); + } + + // Authenticate this request + let thisAndroidEnterprise = await AndroidEnterprise.findOne({ + androidEnterpriseId: androidEnterpriseId + }); + + // Return a 404 response if no records are found. + if (!thisAndroidEnterprise) { + return this.res.notFound(); + } + // Return an unauthorized response if the provided secret does not match. + if (thisAndroidEnterprise.fleetServerSecret !== fleetServerSecret) { + return this.res.unauthorized(); + } + + // Check the list of Android Enterprises managed by Fleet to see if this Android Enterprise is still managed. + let isEnterpriseManagedByFleet = await sails.helpers.androidProxy.getIsEnterpriseManagedByFleet(androidEnterpriseId); + // Return a 404 response if this Android enterprise is no longer managed by Fleet. + if(!isEnterpriseManagedByFleet) { + return this.res.notFound(); + } + + // Get the device for this Android enterprise. + // Note: We're using sails.helpers.flow.build here to handle any errors that occur using google's node library. + let getDeviceResponse = await sails.helpers.flow.build(async () => { + let { google } = require('googleapis'); + let androidmanagement = google.androidmanagement('v1'); + let googleAuth = new google.auth.GoogleAuth({ + scopes: ['https://www.googleapis.com/auth/androidmanagement'], + credentials: { + client_email: sails.config.custom.androidEnterpriseServiceAccountEmailAddress,// eslint-disable-line camelcase + private_key: sails.config.custom.androidEnterpriseServiceAccountPrivateKey,// eslint-disable-line camelcase + }, + }); + // Acquire the google auth client, and bind it to all future calls + let authClient = await googleAuth.getClient(); + google.options({ auth: authClient }); + // [?]: https://googleapis.dev/nodejs/googleapis/latest/androidmanagement/classes/Resource$Enterprises$Devices.html#get + let getDeviceResult = await androidmanagement.enterprises.devices.get({ + name: `enterprises/${androidEnterpriseId}/devices/${deviceId}`, + }); + return getDeviceResult.data; + }).intercept((err) => { + return new Error(`When attempting to get a device for an Android enterprise (${androidEnterpriseId}), an error occurred. Error: ${err}`); + }); + + + // Return the device data back to the Fleet server. + return getDeviceResponse; + + } + + +}; diff --git a/website/api/controllers/android-proxy/modify-android-device.js b/website/api/controllers/android-proxy/modify-android-device.js new file mode 100644 index 0000000000..6106543d96 --- /dev/null +++ b/website/api/controllers/android-proxy/modify-android-device.js @@ -0,0 +1,92 @@ +module.exports = { + + + friendlyName: 'Modify android device', + + + description: 'Modifies a device of an Android enterprise', + + + inputs: { + androidEnterpriseId: { + type: 'string', + required: true, + }, + deviceId: { + type: 'string', + required: true, + }, + }, + + + exits: { + success: { description: 'The device of an Android enterprise was successfully updated.' } + }, + + + fn: async function ({ androidEnterpriseId, deviceId}) { + + // Extract fleetServerSecret from the Authorization header + let authHeader = this.req.get('authorization'); + let fleetServerSecret; + + if (authHeader && authHeader.startsWith('Bearer')) { + fleetServerSecret = authHeader.replace('Bearer', '').trim(); + } else { + return this.res.unauthorized('Authorization header with Bearer token is required'); + } + + // Authenticate this request + let thisAndroidEnterprise = await AndroidEnterprise.findOne({ + androidEnterpriseId: androidEnterpriseId + }); + + // Return a 404 response if no records are found. + if (!thisAndroidEnterprise) { + return this.res.notFound(); + } + // Return an unauthorized response if the provided secret does not match. + if (thisAndroidEnterprise.fleetServerSecret !== fleetServerSecret) { + return this.res.unauthorized(); + } + + // Check the list of Android Enterprises managed by Fleet to see if this Android Enterprise is still managed. + let isEnterpriseManagedByFleet = await sails.helpers.androidProxy.getIsEnterpriseManagedByFleet(androidEnterpriseId); + // Return a 404 response if this Android enterprise is no longer managed by Fleet. + if(!isEnterpriseManagedByFleet) { + return this.res.notFound(); + } + + // Update the device for this Android enterprise. + // Note: We're using sails.helpers.flow.build here to handle any errors that occur using google's node library. + let modifyDeviceResponse = await sails.helpers.flow.build(async () => { + let { google } = require('googleapis'); + let androidmanagement = google.androidmanagement('v1'); + let googleAuth = new google.auth.GoogleAuth({ + scopes: ['https://www.googleapis.com/auth/androidmanagement'], + credentials: { + client_email: sails.config.custom.androidEnterpriseServiceAccountEmailAddress,// eslint-disable-line camelcase + private_key: sails.config.custom.androidEnterpriseServiceAccountPrivateKey,// eslint-disable-line camelcase + }, + }); + // Acquire the google auth client, and bind it to all future calls + let authClient = await googleAuth.getClient(); + google.options({ auth: authClient }); + // [?]: https://googleapis.dev/nodejs/googleapis/latest/androidmanagement/classes/Resource$Enterprises$Devices.html#patch + let patchDeviceResponse = await androidmanagement.enterprises.devices.patch({ + name: `enterprises/${androidEnterpriseId}/devices/${deviceId}`, + requestBody: this.req.body, + }); + return patchDeviceResponse.data; + }).intercept((err) => { + return new Error(`When attempting to update a device for an Android enterprise (${androidEnterpriseId}), an error occurred. Error: ${err}`); + }); + + + // Return the modified device back to the Fleet server. + return modifyDeviceResponse; + + } + + +}; diff --git a/website/config/routes.js b/website/config/routes.js index fdc8c0b97b..e6717663f1 100644 --- a/website/config/routes.js +++ b/website/config/routes.js @@ -1035,6 +1035,9 @@ module.exports.routes = { 'POST /api/android/v1/enterprises/:androidEnterpriseId/enrollmentTokens': { action: 'android-proxy/create-android-enrollment-token', csrf: false}, 'PATCH /api/android/v1/enterprises/:androidEnterpriseId/policies/:policyId': { action: 'android-proxy/modify-android-policies', csrf: false}, 'DELETE /api/android/v1/enterprises/:androidEnterpriseId': { action: 'android-proxy/delete-one-android-enterprise', csrf: false}, + 'GET /api/android/v1/enterprises/:androidEnterpriseId/devices/:deviceId': { action: 'android-proxy/get-android-device', csrf: false}, + 'DELETE /api/android/v1/enterprises/:androidEnterpriseId/devices/:deviceId': { action: 'android-proxy/delete-android-device', csrf: false}, + 'PATCH /api/android/v1/enterprises/:androidEnterpriseId/devices/:deviceId': { action: 'android-proxy/modify-android-device', csrf: false}, // ╔═╗╔═╗╦ ╔═╗╔╗╔╔╦╗╔═╗╔═╗╦╔╗╔╔╦╗╔═╗