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: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 }, 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, }; } };