Skip to content

Commit

Permalink
[IMPROVE] Add API option "permissionsRequired" (#13430)
Browse files Browse the repository at this point in the history
* Add option "permissionsRequired" to API

* Fix apps permissions

* Revert change on unauthorized error response
  • Loading branch information
d-gubert authored and sampaiodiego committed Feb 14, 2019
1 parent c0e54b3 commit 6a044a3
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 64 deletions.
1 change: 1 addition & 0 deletions packages/rocketchat-api/package.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Package.onUse(function(api) {
'rocketchat:models',
'rocketchat:integrations',
'rocketchat:file-upload',
'rocketchat:authorization',
]);

api.mainModule('server/index.js', 'server');
Expand Down
38 changes: 37 additions & 1 deletion packages/rocketchat-api/server/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { RocketChat } from 'meteor/rocketchat:lib';
import { Restivus } from 'meteor/nimble:restivus';
import { Logger } from 'meteor/rocketchat:logger';
import { RateLimiter } from 'meteor/rate-limit';
import { hasAllPermission } from 'meteor/rocketchat:authorization';
import _ from 'underscore';

const logger = new Logger('API', {});
Expand Down Expand Up @@ -123,6 +124,16 @@ class API extends Restivus {
};
}

tooManyRequests(msg) {
return {
statusCode: 429,
body: {
success: false,
error: msg ? msg : 'Too many requests',
},
};
}

addRateLimiterRuleForRoutes({ routes, rateLimiterOptions, endpoints, apiVersion }) {
if (!rateLimiterOptions.numRequestsAllowed) {
throw new Meteor.Error('You must set "numRequestsAllowed" property in rateLimiter for REST API endpoint');
Expand Down Expand Up @@ -159,6 +170,17 @@ class API extends Restivus {
options = {};
}

let shouldVerifyPermissions;

if (!_.isArray(options.permissionsRequired)) {
logger.warn('Invalid value for permissionsRequired');
options.permissionsRequired = undefined;
shouldVerifyPermissions = false;
} else {
shouldVerifyPermissions = !!options.permissionsRequired.length;
}


// Allow for more than one route using the same option and endpoints
if (!_.isArray(routes)) {
routes = [routes];
Expand Down Expand Up @@ -214,11 +236,25 @@ class API extends Restivus {
});
}
}

if (shouldVerifyPermissions && (!this.userId || !hasAllPermission(this.userId, options.permissionsRequired))) {
throw new Meteor.Error('error-unauthorized', 'User does not have the permissions required for this action', {
permissions: options.permissionsRequired,
});
}

result = originalAction.apply(this);
} catch (e) {
logger.debug(`${ method } ${ route } threw an error:`, e.stack);
result = RocketChat.API.v1.failure(e.message, e.error);

const apiMethod = {
'error-too-many-requests': 'tooManyRequests',
'error-unauthorized': 'unauthorized',
}[e.error] || 'failure';

result = RocketChat.API.v1[apiMethod](e.message, e.error);
}

result = result || RocketChat.API.v1.success();

rocketchatRestApiEnd({
Expand Down
112 changes: 57 additions & 55 deletions packages/rocketchat-apps/client/admin/appInstall.html
Original file line number Diff line number Diff line change
@@ -1,64 +1,66 @@
<template name="appInstall">
<section class="preferences-page preferences-page--new">
{{> header sectionName="App_Installation" hideHelp=true fullpage=true}}
<div class="preferences-page__content">
{{#if isInstalling}}
{{> loading}}
{{else}}
<div class="rc-form-group rc-grid">
<div class="rc-input rc-w50 padded">
<label class="rc-input__label">
<div class="rc-input__title">{{>icon icon='clip'}}{{_ "App_Url_to_Install_From"}}</div>
<div class="rc-input__wrapper">
<input type="text" class="rc-input__element" name="appPackageUrl" id="appPackage" placeholder="https://rocket.chat/apps/package.zip" value="{{appUrl}}" >
</div>
</label>
</div>
<div class="rc-input-file rc-w50 padded">
<label class="rc-input-file__label">
<div class="rc-input-file__title">{{>icon icon='clip'}}{{_ "App_Url_to_Install_From_File"}}</div>
<div class="rc-input-file__wrapper">
<div class="rc-input-file__name">
{{appFile}}
{{#requiresPermission 'manage-apps'}}
<div class="preferences-page__content">
{{#if isInstalling}}
{{> loading}}
{{else}}
<div class="rc-form-group rc-grid">
<div class="rc-input rc-w50 padded">
<label class="rc-input__label">
<div class="rc-input__title">{{>icon icon='clip'}}{{_ "App_Url_to_Install_From"}}</div>
<div class="rc-input__wrapper">
<input type="text" class="rc-input__element" name="appPackageUrl" id="appPackage" placeholder="https://rocket.chat/apps/package.zip" value="{{appUrl}}" >
</div>
</label>
</div>
<div class="rc-input-file rc-w50 padded">
<label class="rc-input-file__label">
<div class="rc-input-file__title">{{>icon icon='clip'}}{{_ "App_Url_to_Install_From_File"}}</div>
<div class="rc-input-file__wrapper">
<div class="rc-input-file__name">
{{appFile}}
</div>
<input type="file" accept=".zip" class="rc-input-file__element" name="appPackageUrl" id="upload-app" placeholder="https://rocket.chat/apps/package.zip">
<label for='upload-app' class="rc-button rc-button-secondary install">{{_ "Browse_Files"}}</label>
</div>
<input type="file" accept=".zip" class="rc-input-file__element" name="appPackageUrl" id="upload-app" placeholder="https://rocket.chat/apps/package.zip">
<label for='upload-app' class="rc-button rc-button-secondary install">{{_ "Browse_Files"}}</label>
</div>
</label>
<!-- <input type="file" id="file" class="inputfile" onchange='uploadFile(this)'>
<label for="file">
<span id="file-name" class="file-box"></span>
<span class="file-button">
<i class="fa fa-upload" aria-hidden="true"></i>
Select File
</span>
</label>
</label>
<!-- <input type="file" id="file" class="inputfile" onchange='uploadFile(this)'>
<label for="file">
<span id="file-name" class="file-box"></span>
<span class="file-button">
<i class="fa fa-upload" aria-hidden="true"></i>
Select File
</span>
</label>
<label class="rc-input__label">
<div class="rc-input__title">{{>icon icon='clip'}}{{_ "App_Url_to_Install_From_File"}}</div>
<div class="rc-input__wrapper">
</div>
</label> -->
<!-- <div class="rc-select-avatar__list-item rc-tooltip js-select-app-upload" aria-label="{{_ "Upload_file" }}">
<label class="rc-select-avatar__upload-label app" for="upload-app">
{{> icon block="rc-select-avatar__upload-icon" icon="upload"}}
</label>
<input type="file" name="" value="" id="upload-app" style="display:none;">
</div> -->
<label class="rc-input__label">
<div class="rc-input__title">{{>icon icon='clip'}}{{_ "App_Url_to_Install_From_File"}}</div>
<div class="rc-input__wrapper">
</div>
</label> -->
<!-- <div class="rc-select-avatar__list-item rc-tooltip js-select-app-upload" aria-label="{{_ "Upload_file" }}">
<label class="rc-select-avatar__upload-label app" for="upload-app">
{{> icon block="rc-select-avatar__upload-icon" icon="upload"}}
</label>
<input type="file" name="" value="" id="upload-app" style="display:none;">
</div> -->
</div>
</div>
<div class="rc-button-group">
<button class="rc-button rc-button--secondary js-cancel">{{_ "Cancel"}}</button>
<button class="rc-button rc-button--primary js-install" disabled='{{disabled}}'>
{{#if isUpdating}}
{{_ "Update"}}
{{else}}
{{_ "Install"}}
{{/if}}
</button>
</div>
</div>
<div class="rc-button-group">
<button class="rc-button rc-button--secondary js-cancel">{{_ "Cancel"}}</button>
<button class="rc-button rc-button--primary js-install" disabled='{{disabled}}'>
{{#if isUpdating}}
{{_ "Update"}}
{{else}}
{{_ "Install"}}
{{/if}}
</button>
</div>
{{/if}}
</div>
{{/if}}
</div>
{{/requiresPermission}}
</section>
</template>
16 changes: 8 additions & 8 deletions packages/rocketchat-apps/server/communication/rest.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export class AppsRestApi {
const manager = this._manager;
const fileHandler = this._handleFile;

this.api.addRoute('', { authRequired: true }, {
this.api.addRoute('', { authRequired: true, permissionsRequired: ['manage-apps'] }, {
get() {
const apps = manager.get().map((prl) => {
const info = prl.getInfo();
Expand Down Expand Up @@ -103,7 +103,7 @@ export class AppsRestApi {
},
});

this.api.addRoute(':id', { authRequired: true }, {
this.api.addRoute(':id', { authRequired: true, permissionsRequired: ['manage-apps'] }, {
get() {
console.log('Getting:', this.urlParams.id);
const prl = manager.getOneById(this.urlParams.id);
Expand Down Expand Up @@ -172,7 +172,7 @@ export class AppsRestApi {
},
});

this.api.addRoute(':id/icon', { authRequired: true }, {
this.api.addRoute(':id/icon', { authRequired: true, permissionsRequired: ['manage-apps'] }, {
get() {
console.log('Getting the App\'s Icon:', this.urlParams.id);
const prl = manager.getOneById(this.urlParams.id);
Expand Down Expand Up @@ -202,7 +202,7 @@ export class AppsRestApi {
},
});

this.api.addRoute(':id/logs', { authRequired: true }, {
this.api.addRoute(':id/logs', { authRequired: true, permissionsRequired: ['manage-apps'] }, {
get() {
console.log(`Getting ${ this.urlParams.id }'s logs..`);
const prl = manager.getOneById(this.urlParams.id);
Expand All @@ -228,7 +228,7 @@ export class AppsRestApi {
},
});

this.api.addRoute(':id/settings', { authRequired: true }, {
this.api.addRoute(':id/settings', { authRequired: true, permissionsRequired: ['manage-apps'] }, {
get() {
console.log(`Getting ${ this.urlParams.id }'s settings..`);
const prl = manager.getOneById(this.urlParams.id);
Expand Down Expand Up @@ -274,7 +274,7 @@ export class AppsRestApi {
},
});

this.api.addRoute(':id/settings/:settingId', { authRequired: true }, {
this.api.addRoute(':id/settings/:settingId', { authRequired: true, permissionsRequired: ['manage-apps'] }, {
get() {
console.log(`Getting the App ${ this.urlParams.id }'s setting ${ this.urlParams.settingId }`);

Expand Down Expand Up @@ -315,7 +315,7 @@ export class AppsRestApi {
},
});

this.api.addRoute(':id/apis', { authRequired: true }, {
this.api.addRoute(':id/apis', { authRequired: true, permissionsRequired: ['manage-apps'] }, {
get() {
console.log(`Getting ${ this.urlParams.id }'s apis..`);
const prl = manager.getOneById(this.urlParams.id);
Expand All @@ -330,7 +330,7 @@ export class AppsRestApi {
},
});

this.api.addRoute(':id/status', { authRequired: true }, {
this.api.addRoute(':id/status', { authRequired: true, permissionsRequired: ['manage-apps'] }, {
get() {
console.log(`Getting ${ this.urlParams.id }'s status..`);
const prl = manager.getOneById(this.urlParams.id);
Expand Down

0 comments on commit 6a044a3

Please sign in to comment.