fleet/website/api/controllers/android-proxy/create-android-enterprise.js
Eric b2151decda
Website: Add API rate limit alert to Android proxy endpoints (#35637)
Closes: https://github.com/fleetdm/fleet/issues/34358

Changes:
- Updated Android enterprise proxy endpoints to log an additional
warning to alert us if we exceed the Android management API rate limit.
2025-11-12 15:37:21 -06:00

210 lines
9.2 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}`;
// 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 androidmanagement = google.androidmanagement('v1');
let googleAuth = new google.auth.GoogleAuth({
scopes: [
'https://www.googleapis.com/auth/androidmanagement',
'https://www.googleapis.com/auth/pubsub'
],
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();
let pubsub = google.pubsub({version: 'v1', auth: authClient});
// 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
},
auth: authClient,
});
// 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,
auth: authClient,
});
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:[email protected]']
});
// 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
},
auth: authClient,
});
}).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.
}
},
auth: authClient,
});
// Now create the new enterprise for this Fleet server.
// [?]: https://googleapis.dev/nodejs/googleapis/latest/androidmanagement/classes/Resource$Enterprises.html#create
let createEnterpriseResponse = await androidmanagement.enterprises.create({
agreementAccepted: true,
enterpriseToken: enterpriseToken,
projectId: sails.config.custom.androidEnterpriseProjectId,
signupUrlName: signupUrlName,
requestBody: enterprise,
auth: authClient,
});
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,
};
}
};