Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b40e8ad
Apps Permission System frontend
thassiov Jan 6, 2021
d08cf6e
Ensure the correct order in which modals appear
thassiov Jan 18, 2021
c91c8ed
Merge branch 'develop' into feat/apps-permissions
d-gubert Jan 20, 2021
104be9f
Fix app update on app detail
d-gubert Jan 20, 2021
7bcf4a7
FIx lint errors
d-gubert Jan 20, 2021
fd670cb
Merge remote-tracking branch 'origin/develop' into feat/apps-permissions
d-gubert Jan 20, 2021
8625e4d
Fix update download from Marketplace
d-gubert Jan 20, 2021
b97fc08
Merge remote-tracking branch 'origin/develop' into feat/apps-permissions
d-gubert Jan 21, 2021
52c4689
Add restriction so modal doesn't show if there aren't any permissions
d-gubert Jan 21, 2021
5712b16
Merge remote-tracking branch 'origin/develop' into feat/apps-permissions
d-gubert Jan 21, 2021
c1cc4c6
Add apps-engines permission system's front-end
thassiov Jan 21, 2021
f8a0c47
Adjustments based on linter hints
thassiov Jan 21, 2021
6f7e298
Add package-lock
thassiov Jan 21, 2021
0195798
Fix manual app installation with permissions
d-gubert Jan 22, 2021
b5f022d
Fix handing undefined permissions
d-gubert Jan 22, 2021
1a1129d
Fix wrong prop
d-gubert Jan 22, 2021
1644dbd
Merge remote-tracking branch 'origin/develop' into feat/apps-permissions
d-gubert Jan 22, 2021
d31b2ae
Refactor as suggested by frontend team
d-gubert Jan 22, 2021
608652e
Make sure the tests are able to install apps via url alone
thassiov Jan 22, 2021
fe445e4
Update Apps-Engine version
d-gubert Jan 22, 2021
4b17129
Merge remote-tracking branch 'origin/develop' into feat/apps-permissions
d-gubert Jan 22, 2021
a90156d
Prevent the app to be installed autimatically with url
thassiov Jan 23, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions app/apps/client/orchestrator.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,12 @@ class AppClientOrchestrator {

getAppsFromMarketplace = async () => {
const appsOverviews = await APIClient.get('apps', { marketplace: 'true' });
return appsOverviews.map(({ latest, price, pricingPlans, purchaseType }) => ({
return appsOverviews.map(({ latest, price, pricingPlans, purchaseType, permissions }) => ({
...latest,
price,
pricingPlans,
purchaseType,
permissions,
}));
}

Expand Down Expand Up @@ -125,20 +126,22 @@ class AppClientOrchestrator {
return languages;
}

installApp = async (appId, version) => {
installApp = async (appId, version, permissionsGranted) => {
const { app } = await APIClient.post('apps/', {
appId,
marketplace: true,
version,
permissionsGranted,
});
return app;
}

updateApp = async (appId, version) => {
updateApp = async (appId, version, permissionsGranted) => {
const { app } = await APIClient.post(`apps/${ appId }`, {
appId,
marketplace: true,
version,
permissionsGranted,
});
return app;
}
Expand Down
46 changes: 28 additions & 18 deletions app/apps/server/communication/rest.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,20 @@ export class AppsRestApi {
this.loadAPI();
}

_handleFile(request, fileField) {
_handleMultipartFormData(request) {
const busboy = new Busboy({ headers: request.headers });

return Meteor.wrapAsync((callback) => {
const formFields = {};
busboy.on('file', Meteor.bindEnvironment((fieldname, file) => {
if (fieldname !== fileField) {
return callback(new Meteor.Error('invalid-field', `Expected the field "${ fileField }" but got "${ fieldname }" instead.`));
}

const fileData = [];
file.on('data', Meteor.bindEnvironment((data) => {
fileData.push(data);
}));

file.on('end', Meteor.bindEnvironment(() => callback(undefined, Buffer.concat(fileData))));
file.on('end', Meteor.bindEnvironment(() => { formFields[fieldname] = Buffer.concat(fileData); }));
}));

busboy.on('field', (fieldname, val) => { formFields[fieldname] = val; });
busboy.on('finish', Meteor.bindEnvironment(() => callback(undefined, formFields)));
request.pipe(busboy);
})();
}
Expand All @@ -58,7 +55,7 @@ export class AppsRestApi {
addManagementRoutes() {
const orchestrator = this._orch;
const manager = this._manager;
const fileHandler = this._handleFile;
const multipartFormDataHandler = this._handleMultipartFormData;

const handleError = (message, e) => {
// when there is no `response` field in the error, it means the request
Expand Down Expand Up @@ -171,6 +168,7 @@ export class AppsRestApi {
post() {
let buff;
let marketplaceInfo;
let permissionsGranted;

if (this.bodyParams.url) {
if (settings.get('Apps_Framework_Development_Mode') !== true) {
Expand All @@ -190,6 +188,10 @@ export class AppsRestApi {
}

buff = result.content;

if (this.bodyParams.downloadOnly) {
return API.v1.success({ buff });
}
} else if (this.bodyParams.appId && this.bodyParams.marketplace && this.bodyParams.version) {
const baseUrl = orchestrator.getMarketplaceUrl();

Expand Down Expand Up @@ -233,6 +235,7 @@ export class AppsRestApi {

buff = downloadResult.content;
marketplaceInfo = marketplaceResult.data[0];
permissionsGranted = this.bodyParams.permissionsGranted;
} catch (err) {
return API.v1.failure(err.message);
}
Expand All @@ -241,14 +244,23 @@ export class AppsRestApi {
return API.v1.failure({ error: 'Direct installation of an App is disabled.' });
}

buff = fileHandler(this.request, 'app');
const formData = multipartFormDataHandler(this.request);
buff = formData?.app;
permissionsGranted = (() => {
try {
const permissions = JSON.parse(formData?.permissions || '');
return permissions.length ? permissions : undefined;
} catch {
return undefined;
}
})();
}

if (!buff) {
return API.v1.failure({ error: 'Failed to get a file to install for the App. ' });
}

const aff = Promise.await(manager.add(buff, true, marketplaceInfo));
const aff = Promise.await(manager.add(buff, { marketplaceInfo, permissionsGranted, enable: true }));
const info = aff.getAppInfo();

if (aff.hasStorageError()) {
Expand Down Expand Up @@ -428,18 +440,16 @@ export class AppsRestApi {
const baseUrl = orchestrator.getMarketplaceUrl();

const headers = getDefaultHeaders();
const token = getWorkspaceAccessToken();
if (token) {
headers.Authorization = `Bearer ${ token }`;
}
const token = getWorkspaceAccessToken(true, 'marketplace:download', false);

let result;
try {
result = HTTP.get(`${ baseUrl }/v2/apps/${ this.bodyParams.appId }/download/${ this.bodyParams.version }`, {
result = HTTP.get(`${ baseUrl }/v2/apps/${ this.bodyParams.appId }/download/${ this.bodyParams.version }?token=${ token }`, {
headers,
npmRequestOptions: { encoding: null },
});
} catch (e) {
console.log(e, e.response.content.toString());
orchestrator.getRocketChatLogger().error('Error getting the App from the Marketplace:', e.response.data);
return API.v1.internalError();
}
Expand All @@ -459,14 +469,14 @@ export class AppsRestApi {
return API.v1.failure({ error: 'Direct updating of an App is disabled.' });
}

buff = fileHandler(this.request, 'app');
buff = multipartFormDataHandler(this.request)?.app;
}

if (!buff) {
return API.v1.failure({ error: 'Failed to get a file to install for the App. ' });
}

const aff = Promise.await(manager.update(buff));
const aff = Promise.await(manager.update(buff, this.bodyParams.permissionsGranted));
const info = aff.getAppInfo();

if (aff.hasStorageError()) {
Expand Down
47 changes: 37 additions & 10 deletions client/views/admin/apps/AppInstallPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,25 @@ import { useTranslation } from '../../../contexts/TranslationContext';
import { useFileInput } from '../../../hooks/useFileInput';
import { useForm } from '../../../hooks/useForm';
import { handleInstallError } from './helpers';
import AppPermissionsReviewModal from './AppPermissionsReviewModal';
import { useSetModal } from '../../../contexts/ModalContext';
import { getPermissionsFromZippedApp } from './lib/getPermissionsFromZippedApp';

const placeholderUrl = 'https://rocket.chat/apps/package.zip';

function AppInstallPage() {
const t = useTranslation();

const appsRoute = useRoute('admin-apps');
const setModal = useSetModal();

const appId = useQueryStringParameter('id');
const queryUrl = useQueryStringParameter('url');

const [installing, setInstalling] = useState(false);

const endpointAddress = appId ? `/apps/${ appId }` : '/apps';
const installApp = useEndpoint('POST', endpointAddress);
const downloadApp = useEndpoint('POST', endpointAddress);
const uploadApp = useUpload(endpointAddress);

const { values, handlers } = useForm({
Expand All @@ -48,26 +52,49 @@ function AppInstallPage() {

const [handleUploadButtonClick] = useFileInput(handleFile, 'app');

const install = useCallback(async () => {
const sendFile = async (permissionsGranted, appFile) => {
const fileData = new FormData();
fileData.append('app', appFile, appFile.name);
fileData.append('permissions', JSON.stringify(permissionsGranted));
const { app } = await uploadApp(fileData);
appsRoute.push({ context: 'details', id: app.id });
setModal(null);
};

const cancelAction = useCallback(() => {
setInstalling(false);
setModal(null);
}, [setInstalling, setModal]);

const install = async () => {
setInstalling(true);

try {
let permissions;
let appFile;
if (url) {
const { app } = await installApp({ url });
appsRoute.push({ context: 'details', id: app.id });
return;
const { buff } = await downloadApp({ url, downloadOnly: true });
const fileData = Uint8Array.from(buff.data);
permissions = await getPermissionsFromZippedApp(fileData);
appFile = new File([fileData], 'app.zip', { type: 'application/zip' });
} else {
appFile = file;
permissions = await getPermissionsFromZippedApp(appFile);
}

const fileData = new FormData();
fileData.append('app', file, file.name);
const { app } = await uploadApp(fileData);
appsRoute.push({ context: 'details', id: app.id });
setModal(
<AppPermissionsReviewModal
appPermissions={permissions}
cancel={cancelAction}
confirm={(permissions) => sendFile(permissions, appFile)}
/>,
);
} catch (error) {
handleInstallError(error);
} finally {
setInstalling(false);
}
}, [url, appsRoute, installApp, file, uploadApp]);
};

const handleCancel = () => {
appsRoute.push();
Expand Down
55 changes: 55 additions & 0 deletions client/views/admin/apps/AppPermissionsReviewModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Button, ButtonGroup, Icon, Modal } from '@rocket.chat/fuselage';
import React from 'react';

import { useTranslation } from '../../../contexts/TranslationContext';

const AppPermissionsReviewModal = ({
appPermissions,
cancel,
confirm,
modalProps = {},
}) => {
const t = useTranslation();

const handleCloseButtonClick = () => {
cancel();
};

const handleCancelButtonClick = () => {
cancel();
};

const handleConfirmButtonClick = () => {
confirm(appPermissions);
};


return <Modal {...modalProps}>
<Modal.Header>
<Icon color='danger' name='info-circled' size={20}/>
<Modal.Title>{t('Apps_Permissions_Review_Modal_Title')}</Modal.Title>
<Modal.Close onClick={handleCloseButtonClick}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
<ul>
{
appPermissions.length
? appPermissions.map((permission) =>
<li key={permission.name}>
<b>{ t(`Apps_Permissions_${ permission.name.replace('.', '_') }`) }</b>
{ permission.required && <span style={{ color: 'red' }}> ({ t('Required') })</span> }
</li>)
: t('Apps_Permissions_No_Permissions_Required')
}
</ul>
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button ghost onClick={handleCancelButtonClick}>{t('Cancel')}</Button>
<Button primary onClick={handleConfirmButtonClick}>{t('Accept')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};

export default AppPermissionsReviewModal;
Loading