Files
fleet/website/api/controllers/android-proxy/create-android-enterprise.js
Eric f145c778b8 Website: log number of android enterprise requests in the past minute (#50780)
Related to: https://github.com/fleetdm/fleet/issues/49212

Changes:
- Updated the custom hook to create `sails.androidProxyApiRequestCount`,
and to log and reset the value every minute
- Updated android proxy endpoints to increment
`sails.androidProxyApiRequestCount` every time a request to the Android
management API is sent

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Monitoring**
* Added comprehensive tracking for Android Management API requests
across enrollment, enterprise, device, application, policy, and command
operations.
* Added periodic request-count logging and automatic resets when Android
Enterprise credentials are configured.
* Improved reporting alignment with minute-based API limits while
keeping logs quiet during periods without requests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-07 11:58:35 -05:00

200 lines
9.1 KiB
JavaScript
Vendored

module.exports = {
friendlyName: 'Create android enterprise',
description: 'Creates a new Android enterprise from a request from a Fleet instance.',
inputs: {
signupUrlName: {
type: 'string',
required: true,
},
enterpriseToken: {
type: 'string',
required: true,
},
fleetLicenseKey: {
type: 'string',
},
pubsubPushUrl: {
type: 'string',
required: true,
},
enterprise: {
type: {},
required: true,
moreInfoUrl: ''
}
},
exits: {
success: { description: 'An android enterprise was successfully created' },
enterpriseAlreadyExists: { description: 'An android enterprise already exists for this Fleet instance.', statusCode: 409 },
missingOriginHeader: { description: 'The request was missing an Origin header', responseType: 'badRequest'},
invalidEnterpriseToken: {
description: 'The provided enterprise token is invalid or expired.',
responseType: 'badRequest'
}
},
fn: async function ({signupUrlName, enterpriseToken, fleetLicenseKey, pubsubPushUrl, enterprise}) {
// Parse the Fleet server url from the origin header.
let fleetServerUrl = this.req.get('Origin');
if(!fleetServerUrl){
throw 'missingOriginHeader';
}
// Check the database for a record of this enterprise.
let connectionforThisInstanceExists = await AndroidEnterprise.findOne({fleetServerUrl: fleetServerUrl});
// If this request came from a Fleet instance that already has an enterprise set up, return an error.
if(connectionforThisInstanceExists) {
throw 'enterpriseAlreadyExists';
}
// Generate a uuid to use for the pubsub topic name for this Android enterprise.
let newPubSubTopicName = 'a' + sails.helpers.strings.uuid();// Google requires that topic names start with a letter, so we'll preprend an 'a' to the generated uuid.
// Build the full pubsub topic name.
let fullPubSubTopicName = `projects/${sails.config.custom.androidEnterpriseProjectId}/topics/${newPubSubTopicName}`;
enterprise.pubsubTopic = fullPubSubTopicName;
let newSubscriptionName = `projects/${sails.config.custom.androidEnterpriseProjectId}/subscriptions/${newPubSubTopicName}`;
// Get the shared Google API auth client with the getAndroidManagementAuthorizationClient helper.
// Note: we are doing this outside of the sails.helpers.flow.build() so any errors related to the website's credentials returned by the helper are not intercepted.
let androidManagementAuthClient = await sails.helpers.androidProxy.getAndroidManagementAuthorizationClient();
// Complete the setup of the new Android enterprise.
// Note: We're using sails.helpers.flow.build here to handle any errors that occurr using google's node library.
let newEnterprise = await sails.helpers.flow.build(async ()=>{
let { google } = require('googleapis');
let androidManagementConnection = google.androidmanagement({version: 'v1', auth: androidManagementAuthClient});
let pubsub = google.pubsub({version: 'v1', auth: androidManagementAuthClient});
// Create a new pubsub topic for this enterprise.
// [?]: https://cloud.google.com/pubsub/docs/reference/rest/v1/projects.topics/create
await pubsub.projects.topics.create({
name: fullPubSubTopicName,
requestBody: {
messageRetentionDuration: '86400s'// 24 hours
},
});
// Debugging attempt - Give it a second before calling the getIamPolicy (plus excessive back-off retry delays.)
await sails.helpers.flow.pause(1000);
// [?]: https://cloud.google.com/pubsub/docs/reference/rest/v1/projects.topics/getIamPolicy
// Retrieve the IAM policy for the created pubsub topic.
const newPubSubTopicIamPolicy = await sails.helpers.flow.build(async () => {
const policy = await pubsub.projects.topics.getIamPolicy({
resource: fullPubSubTopicName,
});
return policy.data;
}).retry(undefined, [1000, 2000, 4000]);
// Grant Android device policy the right to publish
// See: https://developers.google.com/android/management/notifications
// Default the policy bindings to an empty array if it is not set.
newPubSubTopicIamPolicy.bindings = newPubSubTopicIamPolicy.bindings || [];
// Add the Fleet android MDM service account to the policy bindings.
newPubSubTopicIamPolicy.bindings.push({
role: 'roles/pubsub.publisher',
members: ['serviceAccount:android-cloud-policy@system.gserviceaccount.com']
});
// Update the pubsub topic's IAM policy
// [?]: https://cloud.google.com/pubsub/docs/reference/rest/v1/projects.topics/setIamPolicy
await sails.helpers.flow.build(async () => {
await pubsub.projects.topics.setIamPolicy({
resource: fullPubSubTopicName,
requestBody: {
policy: newPubSubTopicIamPolicy
},
});
}).retry(undefined, [1000, 1500, 2000]);
// Create a new subscription for the created pubsub topic.
// [?]: https://cloud.google.com/pubsub/docs/reference/rest/v1/projects.subscriptions/create
await pubsub.projects.subscriptions.create({
name: newSubscriptionName,
requestBody: {
topic: fullPubSubTopicName,
ackDeadlineSeconds: 60,
messageRetentionDuration: '86400s',// 24 hours
expirationPolicy: {}, // never expire, so that customers can enable Android but actually enroll devices months later
pushConfig: {
pushEndpoint: pubsubPushUrl// Use the pubsubPushUrl provided by the Fleet server.
}
},
});
// Now create the new enterprise for this Fleet server.
// [?]: https://googleapis.dev/nodejs/googleapis/latest/androidmanagement/classes/Resource$Enterprises.html#create
sails.androidProxyApiRequestCount++;// Count this Android Management API request toward the per-minute total logged in api/hooks/custom/index.js.
let createEnterpriseResponse = await androidManagementConnection.enterprises.create({
agreementAccepted: true,
enterpriseToken: enterpriseToken,
projectId: sails.config.custom.androidEnterpriseProjectId,
signupUrlName: signupUrlName,
requestBody: enterprise,
});
return createEnterpriseResponse.data;
}).intercept({status: 400}, (err)=>{
// Check if it's specifically an invalid token error
let errorString = err.toString();
if (errorString.includes('INVALID_ENTERPRISE_TOKEN') ||
errorString.includes('ExpiredTokenException')) {
return {'invalidEnterpriseToken': 'The provided enterprise token is invalid or expired.'};
}
sails.log.warn('Error details when creating Android enterprise with Android Management API (from 400):', require('util').inspect(err));
// For other 400 errors, still return as invalid token (client error)
return {'invalidEnterpriseToken': 'Invalid request to Android Management API.'};
}).intercept({ status: 401 }, (err) => {
sails.log.warn('Error details when creating Android enterprise with Android Management API (from 401):', require('util').inspect(err));
return {'invalidEnterpriseToken': 'Authorization failed with Android Management API.'};
}).intercept({status: 403}, (err)=>{
sails.log.warn('Error details when creating Android enterprise with Android Management API (from 403):', require('util').inspect(err));
return {'invalidEnterpriseToken': 'Access forbidden to Android Management API.'};
}).intercept({status: 429}, (err)=>{
// If the Android management API returns a 429 response, log an additional warning that will trigger a help-p1 alert.
sails.log.warn(`p1: Android management API rate limit exceeded!`);
return new Error(`When attempting to create a new Android enterprise, an error occurred. Error: ${require('util').inspect(err)}`);
}).intercept((err)=>{
// For all other errors (5XX, network errors, etc.), maintain existing behavior
return new Error(`When attempting to create a new Android enterprise, an error occurred. Error: ${require('util').inspect(err)}`);
});
let newAndroidEnterpriseId = newEnterprise.name;
// Create a new fleetServerSecret for this Fleet server. This will be included in the response body and will be required in all subsequent requests to Android proxy endpoints.
let newFleetServerSecret = await sails.helpers.strings.random.with({len: 30});
// Update the database record to include details about the created enterprise.
await AndroidEnterprise.create({
fleetServerUrl: fleetServerUrl,
fleetLicenseKey: fleetLicenseKey,
androidEnterpriseId: newAndroidEnterpriseId.replace(/enterprises\//, ''),// Remove the /enterprises prefix from the androidEnterpriseId that we save in the website database.
pubsubTopicName: fullPubSubTopicName,
pubsubSubscriptionName: newSubscriptionName,
fleetServerSecret: newFleetServerSecret,
})
.intercept('E_UNIQUE', 'enterpriseAlreadyExists');
return {
name: newAndroidEnterpriseId,
fleetServerSecret: newFleetServerSecret,
};
}
};