fleet/website/api/controllers/android-proxy/create-android-enterprise.js
Magnus Jensen 7e46d63db8
Android Proxy: Pass authClient to all calls (#35456)
This is another attempt at debugging/solving the current android proxy
bug.

This tries to not use the global google options, and instead just passes
the authClient directly to all google calls.

The other PR with wait, did initially succeed for me, but then failed
right after.
2025-11-11 09:29:18 -03:00

206 lines
8.8 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((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,
};
}
};