Website: Fleet Sandbox (#6380)

* create pages, add routes, update policies

* add new pages to importer

* sandbox page

* login -> sandbox-login

* Update login.less

* psuedo-code/code comments

* remove sandbox page

* Revert "remove sandbox page"

This reverts commit d5a1280759266f6bf587c9bab275d1a3e69ec16a.

* view action drafts

* delete forgot-password page

* two new actions + draft code

* change action name

* Draft view actions and page scripts

* Update signup.js

* update comments

* update signup & sandbox-login page script

* update helper comments

* update helper usage in comments

* view-sandbox » view-sandbox-or-redirect

* Update helpers, actions, and routes

* login » sandbox-login

* update attributes on user model

* update signup action

* update page scripts and importer

* Update view-register.js

* html + css

* update signup and view-sandbox-or-redirect

* Password reset

Update user's sandbox password when they have a sandbox instance

* add new-sandbox, update sandbox page

- `/try-fleet/new-sandbox` added for users who don't have an existing Fleet Sandbox instance,
- `/sandbox` updated to redirect users to the `/demologin` endpoint of their Fleet Sandbox instance if it is still valid, or display the sandbox expired state
-  updated policies & routes

* layout and importer updates

* update sandbox-login links & page script

* update signup action

* change logout redirect location to homepage

* lint fixes

* lint fixes

* Update sandbox & sandbox-expired

* Comment updates

* update password requirements for existing pages

* remove /get-started route

* lint fixes

* replace env variable with url

* remove `required: false` from organization attribute on user model

* send redirectToSandbox from view instead of routes

* changes sandbox page name

* add 10 second timeout to /healthz check, add authorization header to cloud provisioner request

* update environment variable name

* update authorization header

* remove /new-sandbox

* update unauthorized response to redirect to correct login screen

* update comments

* update layout

* replace new-sandbox redirects with consistency violation errors

* Provision Fleet sandbox for users logging in

* Revert "Provision Fleet sandbox for users logging in"

This reverts commit 6297c33892231d0ef98bed4cbb127f4263ebc48d.

* Revert "Revert "Provision Fleet sandbox for users logging in""

This reverts commit c2a2567b68325ea92e19f908226de2f52d8265f9.

* Revert "Revert "Revert "Provision Fleet sandbox for users logging in"""

This reverts commit acc178ea76ece637f7f6eab9f44ee51c44f59a00.

* update sandbox-login mobile styles

* update sandbox-expired page to match latest wireframes

* remove required: false and planned changes comments, update signup errors and behavior

* update error

* lint fix on updated error

* Update error's indentation

* remove added forgot-password flow, add redirect for sandbox users changing their password

* Use fleetSandboxDemoKey to login to Fleet Sandbox, remove password changing flow

* update bootstrap to give admin user an expired sandbox

* Update signup.js

* remove unused exits, revert password recovery email changes

* required:false is implied if unspecified, so can be omitted

* Remove defaultsTo: '', since it is not needed

This applies the changes discussed in https://github.com/fleetdm/fleet/pull/6380#discussion_r929538495

It also makes two other trivial changes.

* Eliminate another unnecessary require:false

I think this one is actually baked into the sails-generate template.

* remove custom password validation

* update page name (sandbox-teleporter) and view action name

* revert minor changes to existing files

* update sandbox login friendlyName

* Update unauthorized response to redirect to /login

* Delete new-sandbox.less

* update layouts and importer

* add /fleetctl-preview route for old get-started page, update sandbox route

* update signup action with changes from review, add retry() to cloud provisioner request

* Update routes.js

* add missing comma to route

* update layout, fix typo in signup

* Update sandbox-expired.ejs

* lint fixes

* Update download-sitemap.js

* small whitespace changes, regenerate cloud-sdk

* remove placeholder text in password inputs

* add loading spinner to sandbox teleporter

* add logout button to header nav

* hide header on sandbox-teleporter

* update errors, check if a user already exists before cloud provisioner request

* Update sandbox-teleporter.page.js

* Update sandbox-teleporter.page.js

* Update signup.js

* resize loading spinner, history.pushState() » history.replaceState()

* send users who reset their password back to the fleetdm.com homepage

* Add Zapier webhook request for sandbox signups

* rebuild-scloud-sdk after resolving merge conflict

* update zapier request error

* Add comment w/ context about how Zapier responds with a 2xx even if there was a problem

* Update links to /get-started to go to /try-fleet/register, change /get-started redirect

* Revert changes to links

* add /test-fleet-sandbox redirect, revert /try-fleet redirect

* send logged out users to the sandbox login page when they go to /try-fleet/sandbox

Co-authored-by: Mike McNeil <mikermcneil@users.noreply.github.com>
This commit is contained in:
Eric
2022-08-12 17:31:01 -05:00
committed by GitHub
co-authored by Mike McNeil
parent d728189dd5
commit 7974bdfa80
38 changed files with 1242 additions and 36 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ actually logged in. (If they weren't, then this action is just a no-op.)`,
// > Under the covers, this persists the now-logged-out session back
// > to the underlying session store.
if (!this.req.wantsJSON) {
throw {redirect: '/customers/login'};
throw {redirect: '/'};
}
}
+2 -2
View File
@@ -6,11 +6,11 @@ module.exports = {
description: 'Display "Articles" page.',
inputs: {
category: {
type: 'string',
required: false,
description: 'The category of article to display',
description: 'The category of article to display.',
defaultsTo: '',
}
},
@@ -38,7 +38,6 @@ module.exports = {
},
message: {
required: false,
type: 'string',
description: 'The custom message, in plain text.'
}
+1 -1
View File
@@ -44,7 +44,7 @@ module.exports = {
// ╩ ╩╩ ╩╝╚╝═╩╝ ╚═╝╚═╝═╩╝╚═╝═╩╝ ╩ ╩ ╩╚═╝╚═╝╚═╝
let HAND_CODED_HTML_PAGES = [
'/',
'/get-started',
'/fleetctl-preview',
'/company/contact',
'/queries',
'/platform',
+78 -14
View File
@@ -32,11 +32,10 @@ the account verification message.)`,
type: 'string',
maxLength: 200,
example: 'passwordlol',
description: 'The unencrypted password to use for the new account.'
description: 'The unhashed (plain text) password to use for the new account.'
},
organization: {
required: true,
type: 'string',
maxLength: 120,
example: 'The Sails company',
@@ -84,13 +83,75 @@ the account verification message.)`,
description: 'The provided email address is already in use.',
},
},
},
fn: async function ({emailAddress, password, firstName, lastName, organization, signupReason}) {
if(!sails.config.custom.cloudProvisionerSecret){
throw new Error('The authorization token for the cloud provisioner API (sails.config.custom.cloudProvisionerSecret) is missing! If you just want to test aspects of fleetdm.com locally, and are OK with the cloud provisioner failing if you try to use it, you can set a fake secret when starting a local server by lifting the server with "sails_custom__cloudProvisionerSecret=test sails lift"');
}
var newEmailAddress = emailAddress.toLowerCase();
// Checking if a user with this email address exists in our database before we send a request to the cloud provisioner.
if(await User.findOne({emailAddress: newEmailAddress})) {
throw 'emailAlreadyInUse';
}
// Provisioning a Fleet sandbox instance for the new user. Note: Because this is the only place where we provision Sandbox instances, We'll provision a Sandbox instance BEFORE
// creating the new User record. This way, if this fails, we won't save the new record to the database, and the user will see an error on the signup form asking them to try again.
// Creating an expiration JS timestamp for the Fleet sandbox instance. NOTE: We send this value to the cloud provisioner API as an ISO 8601 string.
let fleetSandboxExpiresAt = Date.now() + (24*60*60*1000);
// Creating a fleetSandboxDemoKey, this will be used for the user's password when we log them into their Sandbox instance.
let fleetSandboxDemoKey = await sails.helpers.strings.uuid();
// Send a POST request to the cloud provisioner API
let cloudProvisionerResponseData = await sails.helpers.http.post(
'https://sandbox.fleetdm.com/new',
{ // Request body
'name': firstName + ' ' + lastName,
'email': newEmailAddress,
'password': fleetSandboxDemoKey, //« this provisioner API was originally designed to accept passwords, but rather than specifying the real plaintext password, since users always access Fleet Sandbox from their fleetdm.com account anyway, this generated demo key is used instead to avoid any confusion
'sandbox_expiration': new Date(fleetSandboxExpiresAt).toISOString(), // sending expiration_timestamp as an ISO string.
},
{ // Request headers
'Authorization':sails.config.custom.cloudProvisionerSecret
}
)
.timeout(5000)
.intercept(['requestFailed', 'non200Response'], (err)=>{
// If we recieved a non-200 response from the cloud provisioner API, we'll throw a 500 error.
return new Error('When attempting to provision a new user who just signed up ('+emailAddress+'), the cloud provisioner gave a non 200 response. The incomplete user record has not been saved in the database, and the user will be asked to try signing up again. Raw response received from provisioner: '+err.stack);
});
if(!cloudProvisionerResponseData.URL) {
// If we didn't receive a URL in the response from the cloud provisioner API, we'll throwing an error before we save the new user record and the user will need to try to sign up again.
throw new Error(
`When provisioning a Fleet Sandbox instance for a new user who just signed up (${emailAddress}), the response data from the cloud provisioner API was malformed. It did not contain a valid Fleet Sandbox instance URL in its expected "URL" property.
The incomplete user record has not been saved in the database, and the user will be asked to try signing up again.
Here is the malformed response data (parsed response body) from the cloud provisioner API: ${cloudProvisionerResponseData}`
);
}
// If "Try Fleet Sandbox" was provided as the signupReason, we'll send a request to Zapier to add this user to our CRM and make sure their Sandbox instance is live before we continue.
if(signupReason === 'Try Fleet Sandbox') {
// Start polling the /healthz endpoint of the created Fleet Sandbox instance, once it returns a 200 response, we'll continue.
await sails.helpers.flow.until( async()=>{
let healthCheckResponse = await sails.helpers.http.sendHttpRequest('GET', cloudProvisionerResponseData.URL+'/healthz')
.timeout(5000)
.tolerate('non200Response')
.tolerate('requestFailed');
if(healthCheckResponse) {
return true;
}
}, 10000).intercept('tookTooLong', ()=>{
return new Error('This newly provisioned Fleet Sandbox instance (for '+emailAddress+') is taking too long to respond with a 2xx status code, even after repeatedly polling the health check endpoint. Note that failed requests and non-2xx responses from the health check endpoint were ignored during polling. Search for a bit of non-dynamic text from this error message in the fleetdm.com source code for more info on exactly how this polling works.');
});
}
// Build up data for the new user record and save it to the database.
// (Also use `fetch` to retrieve the new ID so that we can use it below.)
var newUserRecord = await User.create(_.extend({
@@ -99,6 +160,9 @@ the account verification message.)`,
organization,
emailAddress: newEmailAddress,
password: await sails.helpers.passwords.hashPassword(password),
fleetSandboxURL: cloudProvisionerResponseData.URL,
fleetSandboxExpiresAt,
fleetSandboxDemoKey,
tosAcceptedByIp: this.req.ip
}, sails.config.custom.verifyEmailAddresses? {
emailProofToken: await sails.helpers.strings.random('url-friendly'),
@@ -109,17 +173,6 @@ the account verification message.)`,
.intercept({name: 'UsageError'}, 'invalid')
.fetch();
// If billing feaures are enabled, save a new customer entry in the Stripe API.
// Then persist the Stripe customer id in the database.
if (sails.config.custom.enableBillingFeatures) {
let stripeCustomerId = await sails.helpers.stripe.saveBillingInfo.with({
emailAddress: newEmailAddress
}).timeout(5000).retry();
await User.updateOne({id: newUserRecord.id})
.set({
stripeCustomerId
});
}
// Send a POST request to Zapier
await sails.helpers.http.post(
'https://hooks.zapier.com/hooks/catch/3627242/bqsf4rj/',
@@ -138,6 +191,17 @@ the account verification message.)`,
sails.log.warn(`When a new user signed up, a lead/contact could not be verified in the CRM for this email address: ${newEmailAddress}. Raw error: ${err}`);
return;
});
// If billing feaures are enabled, save a new customer entry in the Stripe API.
// Then persist the Stripe customer id in the database.
if (sails.config.custom.enableBillingFeatures) {
let stripeCustomerId = await sails.helpers.stripe.saveBillingInfo.with({
emailAddress: newEmailAddress
}).timeout(5000).retry();
await User.updateOne({id: newUserRecord.id})
.set({
stripeCustomerId
});
}
// Store the user's new id in their session.
this.req.session.userId = newUserRecord.id;
+5 -1
View File
@@ -24,7 +24,11 @@ module.exports = {
fn: async function () {
if (this.req.me) {
throw {redirect: '/customers/new-license'};
if(this.req.me.hasBillingCard){
throw {redirect: '/customers/new-license'};
} else {
throw {redirect: '/try-fleet/sandbox'};
}
}
return {};
+37
View File
@@ -0,0 +1,37 @@
module.exports = {
friendlyName: 'View register',
description: 'Display "Register" page. Note: This page is the "signup" page skinned for Fleet Sandbox.',
exits: {
success: {
viewTemplatePath: 'pages/try-fleet/register'
},
redirect: {
description: 'The requesting user is already logged in.',
responseType: 'redirect'
}
},
fn: async function () {
// If the user is logged in, redirect them to the Fleet sandbox page.
if (this.req.me) {
throw {redirect: '/try-fleet/sandbox'};
}
// Respond with view.
return {};
}
};
@@ -0,0 +1,27 @@
module.exports = {
friendlyName: 'View sandbox expired',
description: 'Display "Sandbox expired" page.',
exits: {
success: {
viewTemplatePath: 'pages/try-fleet/sandbox-expired'
}
},
fn: async function () {
// Respond with view.
return {};
}
};
+38
View File
@@ -0,0 +1,38 @@
module.exports = {
friendlyName: 'View Sandbox login',
description: 'Display the "Sandbox Login" page. Note: This page is the "login" page skinned for Fleet Sandbox.',
exits: {
success: {
viewTemplatePath: 'pages/try-fleet/sandbox-login'
},
redirect: {
description: 'The requesting user is already logged in.',
responseType: 'redirect'
}
},
fn: async function () {
// If the user is logged in, redirect them to the Fleet sandbox page.
if (this.req.me) {
throw {redirect: '/try-fleet/sandbox'};
}
// Respond with view.
return {};
}
};
@@ -0,0 +1,61 @@
module.exports = {
friendlyName: 'View sandbox teleporter or redirect because sandbox expired',
description:
`Display "Sandbox teleporter" page (an auto-submitting interstitial HTML form used as a hack to grab a bit of HTML
from the Fleet Sandbox instance, which sets browser localstorage to consider this user logged in and "teleports" them,
magically authenticated, into their Fleet Sandbox instance running on a different domain), or redirect the user to
a page about their sandbox instance being expired.`,
moreInfoUrl: 'https://github.com/fleetdm/fleet/pull/6380',
exits: {
success: {
viewTemplatePath: 'pages/try-fleet/sandbox-teleporter',
description: 'This user is being logged into their Fleet Sandbox instance.'
},
redirect: {
description: 'This user does not have a valid Fleet Sandbox instance and is being redirected.',
responseType: 'redirect'
},
},
fn: async function () {
if(!this.req.me) {
throw {redirect: '/try-fleet/login' };
}
if(!this.req.me.fleetSandboxURL) {
throw new Error(`Consistency violation: The logged-in user's (${this.req.me.emailAddress}) fleetSandboxURL has somehow gone missing!`);
}
if(!this.req.me.fleetSandboxExpiresAt) {
throw new Error(`Consistency violation: The logged-in user's (${this.req.me.emailAddress}) fleetSandboxExpiresAt has somehow gone missing!`);
}
if(!this.req.me.fleetSandboxDemoKey) {
throw new Error(`Consistency violation: The logged-in user's (${this.req.me.emailAddress}) fleetSandboxDemoKey has somehow gone missing!`);
}
// If this user's Fleet Sandbox instance is expired, we'll redirect them to the sandbox-expired page
if(this.req.me.fleetSandboxExpiresAt < Date.now()){
throw {redirect: '/try-fleet/sandbox-expired' };
}
// Respond with view.
return {
hideHeaderOnThisPage: true,
};
}
};