[IMPROVE] Add API option "permissionsRequired" (#13430)

* Add option "permissionsRequired" to API

* Fix apps permissions

* Revert change on unauthorized error response
pull/13474/head
Douglas Gubert 7 years ago committed by Diego Sampaio
parent c0e54b37a3
commit 6a044a37f8
No known key found for this signature in database
GPG Key ID: E060152B30502562
  1. 1
      packages/rocketchat-api/package.js
  2. 38
      packages/rocketchat-api/server/api.js
  3. 112
      packages/rocketchat-apps/client/admin/appInstall.html
  4. 16
      packages/rocketchat-apps/server/communication/rest.js

@ -14,6 +14,7 @@ Package.onUse(function(api) {
'rocketchat:models',
'rocketchat:integrations',
'rocketchat:file-upload',
'rocketchat:authorization',
]);
api.mainModule('server/index.js', 'server');

@ -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', {});
@ -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');
@ -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];
@ -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({

@ -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>

@ -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();
@ -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);
@ -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);
@ -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);
@ -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);
@ -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 }`);
@ -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);
@ -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);

Loading…
Cancel
Save