[FIX] Improve cloud section (#13820)

* Improve cloud section

* add sync and fix i18n and a few other issues

* set oauth scopes

* add suggested grammar and federation hub scope

* Switch to postform instead of query string to post oauth more securely
pull/13925/head^2
Aaron Ogle 7 years ago committed by Rodrigo Nascimento
parent 9e2a7532ab
commit b0d71bc943
  1. 40
      app/cloud/client/admin/cloud.html
  2. 85
      app/cloud/client/admin/cloud.js
  3. 33
      app/cloud/server/functions/connectWorkspace.js
  4. 13
      app/cloud/server/functions/disconnectWorkspace.js
  5. 16
      app/cloud/server/functions/finishOAuthAuthorization.js
  6. 5
      app/cloud/server/functions/getOAuthAuthorizationUrl.js
  7. 28
      app/cloud/server/functions/getWorkspaceAccessToken.js
  8. 6
      app/cloud/server/functions/retrieveRegistrationStatus.js
  9. 66
      app/cloud/server/functions/startRegisterWorkspace.js
  10. 58
      app/cloud/server/functions/syncWorkspace.js
  11. 22
      app/cloud/server/functions/unregisterWorkspace.js
  12. 39
      app/cloud/server/methods.js
  13. 16
      app/cloud/server/oauthScopes.js
  14. 14
      app/setup-wizard/client/setupWizard.js
  15. 1
      app/statistics/server/functions/get.js
  16. 7
      packages/rocketchat-i18n/i18n/en.i18n.json

@ -16,25 +16,41 @@
</div>
</div>
<div class="section">
{{#if info.registeredWithWizard}}
{{#if info.workspaceConnected}}
{{#if info.connectToCloud}}
{{#if info.workspaceRegistered}}
<div class="section-content border-component-color">
{{#if info.userAssociated}}
<p>{{_ "Cloud_workspace_connected_plus_account"}}</p>
{{else}}
<p>{{_ "Cloud_workspace_connected_without_account"}}</p>
<p>{{_ "Cloud_workspace_connected"}}</p>
<div class="input-line double-col">
<label class="setting-label" title=""></label>
<div class="setting-field">
<a href="https://cloud.rocket.chat" class="rc-button rc-button--primary" target="_blank">{{_ "Cloud_login_to_cloud"}}</a>
</div>
</div>
</div>
<div class="section-content border-component-color">
<p>{{_ "Cloud_workspace_support"}}</p>
<div class="input-line double-col">
<label class="setting-label" title=""></label>
<div class="setting-field">
<button type="button" class="rc-button rc-button--primary action login-btn">{{_ "Cloud_login_to_cloud"}}</button>
<button type="button" class="rc-button rc-button--danger action sync-btn">{{_ "Sync"}}</button>
</div>
</div>
{{/if}}
</div>
<div class="section-content border-component-color">
<p>{{_ "Cloud_workspace_disconnect"}}</p>
<div class="input-line double-col">
<label class="setting-label" title=""></label>
<div class="setting-field">
<button type="button" class="rc-button rc-button--danger action disconnect-btn">{{_ "Disconnect"}}</button>
</div>
</div>
</div>
{{else}}
<div class="section-content border-component-color">
<div class="input-line double-col">
<label class="setting-label" title="cloudEmail">Email</label>
<label class="setting-label" title="cloudEmail">{{_ "Email"}}</label>
<div class="setting-field">
<input class="input-monitor rc-input__element" type="text" name="cloudEmail" value="{{ info.email }}">
<div class="settings-description secondary-font-color">{{_ "Cloud_address_to_send_registration_to"}}</div>
@ -47,8 +63,6 @@
</div>
</div>
<pre>{{ registeredWithWizard }}</pre>
<div class="input-line double-col">
<label class="setting-label" title="cloudToken">{{_ "Token"}}</label>
<div class="setting-field">
@ -62,6 +76,8 @@
<button type="button" class="rc-button rc-button--primary action connect-btn">{{_ "Connect"}}</button>
</div>
</div>
<p>If you still haven't received a registration email please make sure your email is updated above. If you still have issues you can contact support at: <a href="mailto:support@rocket.chat?subject=[Self Hosted Registration]&body=WorkspaceId: {{ info.workspaceId }}%0D%0ADeployment Id: {{ info.uniqueId }}%0D%0AIssue: <please describe your issue here>">support@rocket.chat</a></p>
</div>
{{/if}}
{{else}}
@ -72,7 +88,7 @@
</div>
<div class="section-content border-component-color">
<p>{{_ "Cloud_registration_required_description"}}</p>
<p><a href="./admin/Setup_Wizard">{{_ "Cloud_registration_requried_link_text"}}</a></p>
<button type="button" class="rc-button rc-button--primary action register-btn">{{_ "Cloud_registration_requried_link_text"}}</button>
</div>
{{/if}}
</div>

@ -34,7 +34,67 @@ Template.cloud.onCreated(function() {
}
if (!success) {
toastr.error('Invalid token');
toastr.error('An error occured connecting');
instance.loadRegStatus();
return;
}
toastr.success(t('Connected'));
instance.loadRegStatus();
});
};
instance.disconnectWorkspace = function _disconnectWorkspace() {
Meteor.call('cloud:disconnectWorkspace', (error, success) => {
if (error) {
toastr.error(error);
instance.loadRegStatus();
return;
}
if (!success) {
toastr.error('An error occured disconnecting');
instance.loadRegStatus();
return;
}
toastr.success(t('Disconnected'));
instance.loadRegStatus();
});
};
instance.syncWorkspace = function _syncWorkspace() {
Meteor.call('cloud:syncWorkspace', (error, success) => {
if (error) {
toastr.error(error);
instance.loadRegStatus();
return;
}
if (!success) {
toastr.error('An error occured syncing');
instance.loadRegStatus();
return;
}
toastr.success(t('Sync Complete'));
instance.loadRegStatus();
});
};
instance.registerWorkspace = function _registerWorkspace() {
Meteor.call('cloud:registerWorkspace', (error, success) => {
if (error) {
toastr.error(error);
instance.loadRegStatus();
return;
}
if (!success) {
toastr.error('An error occured');
instance.loadRegStatus();
return;
}
@ -74,20 +134,21 @@ Template.cloud.events({
});
},
'click .login-btn'() {
Meteor.call('cloud:getOAuthAuthorizationUrl', (error, url) => {
if (error) {
console.warn(error);
return;
}
window.location.href = url;
});
},
'click .connect-btn'(e, i) {
const token = $('input[name=cloudToken]').val();
i.connectWorkspace(token);
},
'click .register-btn'(e, i) {
i.registerWorkspace();
},
'click .disconnect-btn'(e, i) {
i.disconnectWorkspace();
},
'click .sync-btn'(e, i) {
i.syncWorkspace();
},
});

@ -1,16 +1,15 @@
import querystring from 'querystring';
import { HTTP } from 'meteor/http';
import { settings } from '../../../settings';
import { Settings } from '../../../models';
import { getRedirectUri } from './getRedirectUri';
import { retrieveRegistrationStatus } from './retrieveRegistrationStatus';
import { getWorkspaceAccessToken } from './getWorkspaceAccessToken';
export function connectWorkspace(token) {
const { registeredWithWizard } = retrieveRegistrationStatus();
if (!registeredWithWizard) {
return false;
const { connectToCloud } = retrieveRegistrationStatus();
if (!connectToCloud) {
Settings.updateValueById('Register_Server', true);
}
const redirectUri = getRedirectUri();
@ -31,6 +30,10 @@ export function connectWorkspace(token) {
data: regInfo,
});
} catch (e) {
if (e.response && e.response.data && e.response.data.error) {
console.error(`Failed to register with Rocket.Chat Cloud. Error: ${ e.response.data.error }`);
}
return false;
}
@ -48,26 +51,10 @@ export function connectWorkspace(token) {
Settings.updateValueById('Cloud_Workspace_Registration_Client_Uri', data.registration_client_uri);
// Now that we have the client id and secret, let's get the access token
let authTokenResult;
try {
authTokenResult = HTTP.post(`${ cloudUrl }/api/oauth/token`, {
data: {},
query: querystring.stringify({
client_id: data.client_id,
client_secret: data.client_secret,
grant_type: 'client_credentials',
redirect_uri: redirectUri,
}),
});
} catch (e) {
const accessToken = getWorkspaceAccessToken(true);
if (!accessToken) {
return false;
}
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + authTokenResult.data.expires_in);
Settings.updateValueById('Cloud_Workspace_Access_Token', authTokenResult.data.access_token);
Settings.updateValueById('Cloud_Workspace_Access_Token_Expires_At', expiresAt);
return true;
}

@ -0,0 +1,13 @@
import { retrieveRegistrationStatus } from './retrieveRegistrationStatus';
import { Settings } from '../../../models';
export function disconnectWorkspace() {
const { connectToCloud } = retrieveRegistrationStatus();
if (!connectToCloud) {
return true;
}
Settings.updateValueById('Register_Server', false);
return true;
}

@ -1,11 +1,10 @@
import querystring from 'querystring';
import { Meteor } from 'meteor/meteor';
import { HTTP } from 'meteor/http';
import { settings } from '../../../settings';
import { Settings, Users } from '../../../models';
import { getRedirectUri } from './getRedirectUri';
import { userScopes } from '../oauthScopes';
export function finishOAuthAuthorization(code, state) {
if (settings.get('Cloud_Workspace_Registration_State') !== state) {
@ -16,19 +15,26 @@ export function finishOAuthAuthorization(code, state) {
const clientId = settings.get('Cloud_Workspace_Client_Id');
const clientSecret = settings.get('Cloud_Workspace_Client_Secret');
const scope = userScopes.join(' ');
let result;
try {
result = HTTP.post(`${ cloudUrl }/api/oauth/token`, {
data: {},
query: querystring.stringify({
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
params: {
client_id: clientId,
client_secret: clientSecret,
grant_type: 'authorization_code',
scope,
code,
redirect_uri: getRedirectUri(),
}),
},
});
} catch (e) {
if (e.response && e.response.data && e.response.data.error) {
console.error(`Failed to get AccessToken from Rocket.Chat Cloud. Error: ${ e.response.data.error }`);
}
return false;
}

@ -3,6 +3,7 @@ import { Settings } from '../../../models';
import { settings } from '../../../settings';
import { getRedirectUri } from './getRedirectUri';
import { userScopes } from '../oauthScopes';
export function getOAuthAuthorizationUrl() {
const state = Random.id();
@ -13,5 +14,7 @@ export function getOAuthAuthorizationUrl() {
const client_id = settings.get('Cloud_Workspace_Client_Id');
const redirectUri = getRedirectUri();
return `${ cloudUrl }/authorize?response_type=code&client_id=${ client_id }&redirect_uri=${ redirectUri }&scope=offline_access&state=${ state }`;
const scope = userScopes.join(' ');
return `${ cloudUrl }/authorize?response_type=code&client_id=${ client_id }&redirect_uri=${ redirectUri }&scope=${ scope }&state=${ state }`;
}

@ -1,13 +1,16 @@
import querystring from 'querystring';
import { HTTP } from 'meteor/http';
import { settings } from '../../../settings';
import { Settings } from '../../../models';
import { getRedirectUri } from './getRedirectUri';
import { retrieveRegistrationStatus } from './retrieveRegistrationStatus';
import { unregisterWorkspace } from './unregisterWorkspace';
import { workspaceScopes } from '../oauthScopes';
export function getWorkspaceAccessToken(forceNew = false, scope = '', save = true) {
if (!settings.get('Register_Server')) {
const { connectToCloud, workspaceRegistered } = retrieveRegistrationStatus();
if (!connectToCloud || !workspaceRegistered) {
return '';
}
@ -27,19 +30,32 @@ export function getWorkspaceAccessToken(forceNew = false, scope = '', save = tru
const client_secret = settings.get('Cloud_Workspace_Client_Secret');
const redirectUri = getRedirectUri();
if (scope === '') {
scope = workspaceScopes.join(' ');
}
let authTokenResult;
try {
authTokenResult = HTTP.post(`${ cloudUrl }/api/oauth/token`, {
data: {},
query: querystring.stringify({
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
params: {
client_id,
client_secret,
scope,
grant_type: 'client_credentials',
redirect_uri: redirectUri,
}),
},
});
} catch (e) {
if (e.response && e.response.data && e.response.data.error) {
console.error(`Failed to get AccessToken from Rocket.Chat Cloud. Error: ${ e.response.data.error }`);
if (e.response.data.error === 'oauth_invalid_client_credentials') {
console.error('Server has been unregistered from cloud');
unregisterWorkspace();
}
}
return '';
}

@ -3,9 +3,11 @@ import { Users } from '../../../models';
export function retrieveRegistrationStatus() {
const info = {
registeredWithWizard: settings.get('Register_Server'),
workspaceConnected: (settings.get('Cloud_Workspace_Client_Id')) ? true : false,
connectToCloud: settings.get('Register_Server'),
workspaceRegistered: (settings.get('Cloud_Workspace_Client_Id')) ? true : false,
userAssociated: (settings.get('Cloud_Workspace_Account_Associated')) ? true : false,
workspaceId: settings.get('Cloud_Workspace_Id'),
uniqueId: settings.get('uniqueID'),
token: '',
email: '',
};

@ -0,0 +1,66 @@
import { HTTP } from 'meteor/http';
import { settings } from '../../../settings';
import { Settings } from '../../../models';
import { retrieveRegistrationStatus } from './retrieveRegistrationStatus';
import { statistics } from '../../../statistics';
export function startRegisterWorkspace() {
const { workspaceRegistered, connectToCloud } = retrieveRegistrationStatus();
if ((workspaceRegistered && connectToCloud) || process.env.TEST_MODE) {
return true;
}
settings.updateById('Register_Server', true);
if (workspaceRegistered) {
return true;
}
const stats = statistics.get();
const address = settings.get('Site_Url');
// If we have it lets send it because likely an update
const workspaceId = settings.get('Cloud_Workspace_Id');
const regInfo = {
uniqueId: stats.uniqueId,
workspaceId,
address,
contactName: stats.wizard.contactName,
contactEmail: stats.wizard.contactEmail,
accountName: stats.wizard.organizationName,
siteName: stats.wizard.siteName,
deploymentMethod: stats.deploy.method,
deploymentPlatform: stats.deploy.platform,
version: stats.version,
};
const cloudUrl = settings.get('Cloud_Url');
let result;
try {
result = HTTP.post(`${ cloudUrl }/api/v2/register/workspace`, {
data: regInfo,
});
} catch (e) {
if (e.response && e.response.data && e.response.data.errorCode) {
console.error(`Failed to register with Rocket.Chat Cloud. ErrorCode: ${ e.response.data.errorCode }`);
}
return false;
}
const { data } = result;
if (!data) {
return false;
}
Settings.updateValueById('Cloud_Workspace_Id', data.id);
console.log(data);
return true;
}

@ -0,0 +1,58 @@
import { HTTP } from 'meteor/http';
import { settings } from '../../../settings';
import { retrieveRegistrationStatus } from './retrieveRegistrationStatus';
import { getWorkspaceAccessToken } from './getWorkspaceAccessToken';
import { statistics } from '../../../statistics';
import { getWorkspaceLicense } from './getWorkspaceLicense';
export function syncWorkspace() {
const { workspaceRegistered, connectToCloud } = retrieveRegistrationStatus();
if (!workspaceRegistered || !connectToCloud) {
return false;
}
const stats = statistics.get();
const address = settings.get('Site_Url');
const info = {
uniqueId: stats.uniqueId,
address,
contactName: stats.wizard.contactName,
contactEmail: stats.wizard.contactEmail,
accountName: stats.wizard.organizationName,
siteName: stats.wizard.siteName,
deploymentMethod: stats.deploy.method,
deploymentPlatform: stats.deploy.platform,
version: stats.version,
};
const workspaceUrl = settings.get('Cloud_Workspace_Registration_Client_Uri');
try {
const headers = {};
const token = getWorkspaceAccessToken(true);
if (token) {
headers.Authorization = `Bearer ${ token }`;
} else {
return false;
}
HTTP.post(`${ workspaceUrl }/registration`, {
data: info,
headers,
});
} catch (e) {
if (e.response && e.response.data && e.response.data.error) {
console.error(`Failed to sync with Rocket.Chat Cloud. Error: ${ e.response.data.error }`);
}
return false;
}
return getWorkspaceLicense();
}

@ -0,0 +1,22 @@
import { Settings } from '../../../models';
import { retrieveRegistrationStatus } from './retrieveRegistrationStatus';
export function unregisterWorkspace() {
const { workspaceRegistered } = retrieveRegistrationStatus();
if (!workspaceRegistered) {
return true;
}
Settings.updateValueById('Cloud_Workspace_Id', null);
Settings.updateValueById('Cloud_Workspace_Name', null);
Settings.updateValueById('Cloud_Workspace_Client_Id', null);
Settings.updateValueById('Cloud_Workspace_Client_Secret', null);
Settings.updateValueById('Cloud_Workspace_Client_Secret_Expires_At', null);
Settings.updateValueById('Cloud_Workspace_Registration_Client_Uri', null);
// So doesn't try to register again automatically
Settings.updateValueById('Register_Server', false);
return true;
}

@ -7,6 +7,9 @@ import { retrieveRegistrationStatus } from './functions/retrieveRegistrationStat
import { connectWorkspace } from './functions/connectWorkspace';
import { getOAuthAuthorizationUrl } from './functions/getOAuthAuthorizationUrl';
import { finishOAuthAuthorization } from './functions/finishOAuthAuthorization';
import { startRegisterWorkspace } from './functions/startRegisterWorkspace';
import { disconnectWorkspace } from './functions/disconnectWorkspace';
import { syncWorkspace } from './functions/syncWorkspace';
Meteor.methods({
'cloud:checkRegisterStatus'() {
@ -20,6 +23,17 @@ Meteor.methods({
return retrieveRegistrationStatus();
},
'cloud:registerWorkspace'() {
if (!Meteor.userId()) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'cloud:startRegister' });
}
if (!hasPermission(Meteor.userId(), 'manage-cloud')) {
throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'cloud:startRegister' });
}
return startRegisterWorkspace();
},
'cloud:updateEmail'(email) {
check(email, String);
@ -32,6 +46,19 @@ Meteor.methods({
}
Settings.updateValueById('Organization_Email', email);
return startRegisterWorkspace();
},
'cloud:syncWorkspace'() {
if (!Meteor.userId()) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'cloud:updateEmail' });
}
if (!hasPermission(Meteor.userId(), 'manage-cloud')) {
throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'cloud:updateEmail' });
}
return syncWorkspace();
},
'cloud:connectWorkspace'(token) {
check(token, String);
@ -46,6 +73,18 @@ Meteor.methods({
return connectWorkspace(token);
},
'cloud:disconnectWorkspace'() {
if (!Meteor.userId()) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'cloud:connectServer' });
}
if (!hasPermission(Meteor.userId(), 'manage-cloud')) {
throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'cloud:connectServer' });
}
return disconnectWorkspace();
},
// Currently unused but will link local account to Rocket.Chat Cloud account.
'cloud:getOAuthAuthorizationUrl'() {
if (!Meteor.userId()) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'cloud:connectServer' });

@ -0,0 +1,16 @@
// These are the scopes we by default request access to
export const workspaceScopes = [
'workspace:license:read',
'workspace:client:write',
'workspace:stats:write',
'workspace:push:send',
'marketplace:read',
'marketplace:download',
'fedhub:register',
];
// These are the scopes we use for the user
export const userScopes = [
'openid',
'offline_access',
];

@ -193,7 +193,19 @@ Template.setupWizard.events({
persistSettings(t.state.all(), () => {
localStorage.removeItem('wizard');
localStorage.setItem('wizardFinal', true);
FlowRouter.go('setup-wizard-final');
if (t.state.get('registerServer')) {
Meteor.call('cloud:registerWorkspace', (error) => {
if (error) {
console.warn(error);
return;
}
FlowRouter.go('setup-wizard-final');
});
} else {
FlowRouter.go('setup-wizard-final');
}
});
return false;
}

@ -20,6 +20,7 @@ const wizardFields = [
'Language',
'Server_Type',
'Allow_Marketing_Emails',
'Register_Server',
];
statistics.get = function _getStatistics() {

@ -602,8 +602,9 @@
"Cloud_connect": "Rocket.Chat Cloud Connect",
"Cloud_what_is_it": "What is this?",
"Cloud_what_is_it_description": "Rocket.Chat Cloud Connect allows you to connect your self-hosted Rocket.Chat Workspace to our Cloud. Doing so enables you to manage your licenses, Billing and Support in Rocket.Chat Cloud.",
"Cloud_workspace_connected_plus_account": "Your workspace is now connected to the Rocket.Chat Cloud and an account is associated.",
"Cloud_workspace_connected_without_account": "Your workspace is now connected to the Rocket.Chat Cloud. If you would like, you can login to the Rocket.Chat Cloud and associate your workspace with your Cloud account.",
"Cloud_workspace_connected": "Your workspace has been successfully connected to Rocket.Chat Cloud. You can access the cloud to manage account information",
"Cloud_workspace_support": "If you have any trouble with a cloud service, please try to sync first. Should the issue persist, please open a support ticket in the Cloud Console.",
"Cloud_workspace_disconnect": "If you no longer wish to utilize cloud services you can disconnect your workspace from the Rocket.Chat Cloud.",
"Cloud_login_to_cloud": "Login to Rocket.Chat Cloud",
"Cloud_address_to_send_registration_to": "The address to send your Cloud registration email to.",
"Cloud_update_email": "Update Email",
@ -1037,6 +1038,7 @@
"Disabled": "Disabled",
"Disallow_reacting": "Disallow Reacting",
"Disallow_reacting_Description": "Disallows reacting",
"Disconnect": "Disconnect",
"Display_offline_form": "Display Offline Form",
"Display_unread_counter": "Display number of unread messages",
"Displays_action_text": "Displays action text",
@ -2719,6 +2721,7 @@
"Survey": "Survey",
"Survey_instructions": "Rate each question according to your satisfaction, 1 meaning you are completely unsatisfied and 5 meaning you are completely satisfied.",
"Symbols": "Symbols",
"Sync": "Sync",
"Sync / Import": "Sync / Import",
"Sync_in_progress": "Synchronization in progress",
"Sync_Interval": "Sync interval",

Loading…
Cancel
Save