mirror of
https://github.com/fleetdm/fleet
synced 2026-04-30 18:07:56 +00:00
211 lines
9.2 KiB
JavaScript
211 lines
9.2 KiB
JavaScript
|
|
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,
|
||
|
|
};
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
};
|