Unverified Commit e9811658 authored by Gung Wah's avatar Gung Wah Committed by GitHub
Browse files

[CHORE] Add permissions to Redux (#2914)



* [FIX] Add permissions to Redux store

* add only permissions being used in the app

* add clear permissions reducer

* call RocketChat.hasPermission from reducer

* add server version comparison on getPermissions

* refactor hasPermission function

* refactor hasPermission function

* remove uncomment code

* use Q.experimentalSortBy()

* add coerce function

* Change Rocketchat.hasPermission

* Apply on isReadOnly

* Apply to RoomInfoEditView

* Apply to RoomInfoView and RoomInfoEditView

* canAutoTranslate

* Unnecessary clear permissions

* Revert getUpdatedSince

* Naming fix
Co-authored-by: default avatarDiego Mello <diegolmello@gmail.com>
parent 6e32a15c
...@@ -70,3 +70,5 @@ export const SETTINGS = createRequestTypes('SETTINGS', ['CLEAR', 'ADD']); ...@@ -70,3 +70,5 @@ export const SETTINGS = createRequestTypes('SETTINGS', ['CLEAR', 'ADD']);
export const APP_STATE = createRequestTypes('APP_STATE', ['FOREGROUND', 'BACKGROUND']); export const APP_STATE = createRequestTypes('APP_STATE', ['FOREGROUND', 'BACKGROUND']);
export const ENTERPRISE_MODULES = createRequestTypes('ENTERPRISE_MODULES', ['CLEAR', 'SET']); export const ENTERPRISE_MODULES = createRequestTypes('ENTERPRISE_MODULES', ['CLEAR', 'SET']);
export const ENCRYPTION = createRequestTypes('ENCRYPTION', ['INIT', 'STOP', 'DECODE_KEY', 'SET', 'SET_BANNER']); export const ENCRYPTION = createRequestTypes('ENCRYPTION', ['INIT', 'STOP', 'DECODE_KEY', 'SET', 'SET_BANNER']);
export const PERMISSIONS = createRequestTypes('PERMISSIONS', ['SET']);
import * as types from './actionsTypes';
export function setPermissions(permissions) {
return {
type: types.PERMISSIONS.SET,
permissions
};
}
...@@ -34,20 +34,24 @@ const MessageActions = React.memo(forwardRef(({ ...@@ -34,20 +34,24 @@ const MessageActions = React.memo(forwardRef(({
Message_AllowPinning, Message_AllowPinning,
Message_AllowStarring, Message_AllowStarring,
Message_Read_Receipt_Store_Users, Message_Read_Receipt_Store_Users,
isMasterDetail isMasterDetail,
editMessagePermission,
deleteMessagePermission,
forceDeleteMessagePermission,
pinMessagePermission
}, ref) => { }, ref) => {
let permissions = {}; let permissions = {};
const { showActionSheet, hideActionSheet } = useActionSheet(); const { showActionSheet, hideActionSheet } = useActionSheet();
const getPermissions = async() => { const getPermissions = async() => {
try { try {
const permission = ['edit-message', 'delete-message', 'force-delete-message', 'pin-message']; const permission = [editMessagePermission, deleteMessagePermission, forceDeleteMessagePermission, pinMessagePermission];
const result = await RocketChat.hasPermission(permission, room.rid); const result = await RocketChat.hasPermission(permission, room.rid);
permissions = { permissions = {
hasEditPermission: result[permission[0]], hasEditPermission: result[0],
hasDeletePermission: result[permission[1]], hasDeletePermission: result[1],
hasForceDeletePermission: result[permission[2]], hasForceDeletePermission: result[2],
hasPinPermission: result[permission[3]] hasPinPermission: result[3]
}; };
} catch { } catch {
// Do nothing // Do nothing
...@@ -440,7 +444,11 @@ MessageActions.propTypes = { ...@@ -440,7 +444,11 @@ MessageActions.propTypes = {
Message_AllowPinning: PropTypes.bool, Message_AllowPinning: PropTypes.bool,
Message_AllowStarring: PropTypes.bool, Message_AllowStarring: PropTypes.bool,
Message_Read_Receipt_Store_Users: PropTypes.bool, Message_Read_Receipt_Store_Users: PropTypes.bool,
server: PropTypes.string server: PropTypes.string,
editMessagePermission: PropTypes.array,
deleteMessagePermission: PropTypes.array,
forceDeleteMessagePermission: PropTypes.array,
pinMessagePermission: PropTypes.array
}; };
const mapStateToProps = state => ({ const mapStateToProps = state => ({
...@@ -452,7 +460,11 @@ const mapStateToProps = state => ({ ...@@ -452,7 +460,11 @@ const mapStateToProps = state => ({
Message_AllowPinning: state.settings.Message_AllowPinning, Message_AllowPinning: state.settings.Message_AllowPinning,
Message_AllowStarring: state.settings.Message_AllowStarring, Message_AllowStarring: state.settings.Message_AllowStarring,
Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users, Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users,
isMasterDetail: state.app.isMasterDetail isMasterDetail: state.app.isMasterDetail,
editMessagePermission: state.permissions['edit-message'],
deleteMessagePermission: state.permissions['delete-message'],
forceDeleteMessagePermission: state.permissions['force-delete-message'],
pinMessagePermission: state.permissions['pin-message']
}); });
export default connect(mapStateToProps, null, null, { forwardRef: true })(MessageActions); export default connect(mapStateToProps, null, null, { forwardRef: true })(MessageActions);
import lt from 'semver/functions/lt'; import lt from 'semver/functions/lt';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import { Q } from '@nozbe/watermelondb';
import coerce from 'semver/functions/coerce';
import orderBy from 'lodash/orderBy'; import orderBy from 'lodash/orderBy';
import database from '../database'; import database from '../database';
import log from '../../utils/log'; import log from '../../utils/log';
import reduxStore from '../createStore'; import reduxStore from '../createStore';
import protectedFunction from './helpers/protectedFunction'; import protectedFunction from './helpers/protectedFunction';
import { setPermissions as setPermissionsAction } from '../../actions/permissions';
const PERMISSIONS = [
'add-user-to-any-c-room',
'add-user-to-any-p-room',
'add-user-to-joined-room',
'archive-room',
'auto-translate',
'create-invite-links',
'delete-c',
'delete-message',
'delete-p',
'edit-message',
'edit-room',
'force-delete-message',
'mute-user',
'pin-message',
'post-readonly',
'remove-user',
'set-leader',
'set-moderator',
'set-owner',
'set-react-when-readonly',
'set-readonly',
'toggle-room-e2e-encryption',
'transfer-livechat-guest',
'unarchive-room',
'view-broadcast-member-list',
'view-privileged-setting',
'view-room-administration',
'view-statistics',
'view-user-administration'
];
export async function setPermissions() {
const db = database.active;
const permissionsCollection = db.collections.get('permissions');
const allPermissions = await permissionsCollection.query(Q.where('id', Q.oneOf(PERMISSIONS))).fetch();
const parsed = allPermissions.reduce((acc, item) => ({ ...acc, [item.id]: item.roles }), {});
reduxStore.dispatch(setPermissionsAction(parsed));
}
const getUpdatedSince = (allRecords) => { const getUpdatedSince = (allRecords) => {
try { try {
...@@ -64,12 +108,13 @@ const updatePermissions = async({ update = [], remove = [], allRecords }) => { ...@@ -64,12 +108,13 @@ const updatePermissions = async({ update = [], remove = [], allRecords }) => {
await db.action(async() => { await db.action(async() => {
await db.batch(...batch); await db.batch(...batch);
}); });
return true;
} catch (e) { } catch (e) {
log(e); log(e);
} }
}; };
export default function() { export function getPermissions() {
return new Promise(async(resolve) => { return new Promise(async(resolve) => {
try { try {
const serverVersion = reduxStore.getState().server.version; const serverVersion = reduxStore.getState().server.version;
...@@ -78,17 +123,20 @@ export default function() { ...@@ -78,17 +123,20 @@ export default function() {
const allRecords = await permissionsCollection.query().fetch(); const allRecords = await permissionsCollection.query().fetch();
// if server version is lower than 0.73.0, fetches from old api // if server version is lower than 0.73.0, fetches from old api
if (serverVersion && lt(serverVersion, '0.73.0')) { if (serverVersion && lt(coerce(serverVersion), '0.73.0')) {
// RC 0.66.0 // RC 0.66.0
const result = await this.sdk.get('permissions.list'); const result = await this.sdk.get('permissions.list');
if (!result.success) { if (!result.success) {
return resolve(); return resolve();
} }
await updatePermissions({ update: result.permissions, allRecords }); const changePermissions = await updatePermissions({ update: result.permissions, allRecords });
if (changePermissions) {
setPermissions();
}
return resolve(); return resolve();
} else { } else {
const params = {}; const params = {};
const updatedSince = await getUpdatedSince(allRecords); const updatedSince = getUpdatedSince(allRecords);
if (updatedSince) { if (updatedSince) {
params.updatedSince = updatedSince; params.updatedSince = updatedSince;
} }
...@@ -99,7 +147,10 @@ export default function() { ...@@ -99,7 +147,10 @@ export default function() {
return resolve(); return resolve();
} }
await updatePermissions({ update: result.update, remove: result.delete, allRecords }); const changePermissions = await updatePermissions({ update: result.update, remove: result.delete, allRecords });
if (changePermissions) {
setPermissions();
}
return resolve(); return resolve();
} }
} catch (e) { } catch (e) {
......
...@@ -32,7 +32,7 @@ import readMessages from './methods/readMessages'; ...@@ -32,7 +32,7 @@ import readMessages from './methods/readMessages';
import getSettings, { getLoginSettings, setSettings } from './methods/getSettings'; import getSettings, { getLoginSettings, setSettings } from './methods/getSettings';
import getRooms from './methods/getRooms'; import getRooms from './methods/getRooms';
import getPermissions from './methods/getPermissions'; import { setPermissions, getPermissions } from './methods/getPermissions';
import { getCustomEmojis, setCustomEmojis } from './methods/getCustomEmojis'; import { getCustomEmojis, setCustomEmojis } from './methods/getCustomEmojis';
import { import {
getEnterpriseModules, setEnterpriseModules, hasLicense, isOmnichannelModuleAvailable getEnterpriseModules, setEnterpriseModules, hasLicense, isOmnichannelModuleAvailable
...@@ -70,7 +70,6 @@ const CERTIFICATE_KEY = 'RC_CERTIFICATE_KEY'; ...@@ -70,7 +70,6 @@ const CERTIFICATE_KEY = 'RC_CERTIFICATE_KEY';
export const THEME_PREFERENCES_KEY = 'RC_THEME_PREFERENCES_KEY'; export const THEME_PREFERENCES_KEY = 'RC_THEME_PREFERENCES_KEY';
export const CRASH_REPORT_KEY = 'RC_CRASH_REPORT_KEY'; export const CRASH_REPORT_KEY = 'RC_CRASH_REPORT_KEY';
export const ANALYTICS_EVENTS_KEY = 'RC_ANALYTICS_EVENTS_KEY'; export const ANALYTICS_EVENTS_KEY = 'RC_ANALYTICS_EVENTS_KEY';
const returnAnArray = obj => obj || [];
const MIN_ROCKETCHAT_VERSION = '0.70.0'; const MIN_ROCKETCHAT_VERSION = '0.70.0';
const STATUSES = ['offline', 'online', 'away', 'busy']; const STATUSES = ['offline', 'online', 'away', 'busy'];
...@@ -740,6 +739,7 @@ const RocketChat = { ...@@ -740,6 +739,7 @@ const RocketChat = {
getLoginSettings, getLoginSettings,
setSettings, setSettings,
getPermissions, getPermissions,
setPermissions,
getCustomEmojis, getCustomEmojis,
setCustomEmojis, setCustomEmojis,
getEnterpriseModules, getEnterpriseModules,
...@@ -1172,10 +1172,13 @@ const RocketChat = { ...@@ -1172,10 +1172,13 @@ const RocketChat = {
// RC 0.65.0 // RC 0.65.0
return this.sdk.get(`${ this.roomTypeToApiType(type) }.roles`, { roomId }); return this.sdk.get(`${ this.roomTypeToApiType(type) }.roles`, { roomId });
}, },
/**
* Permissions: array of permissions' roles from redux. Example: [['owner', 'admin'], ['leader']]
* Returns an array of boolean for each permission from permissions arg
*/
async hasPermission(permissions, rid) { async hasPermission(permissions, rid) {
const db = database.active; const db = database.active;
const subsCollection = db.collections.get('subscriptions'); const subsCollection = db.collections.get('subscriptions');
const permissionsCollection = db.collections.get('permissions');
let roomRoles = []; let roomRoles = [];
try { try {
// get the room from database // get the room from database
...@@ -1184,31 +1187,16 @@ const RocketChat = { ...@@ -1184,31 +1187,16 @@ const RocketChat = {
roomRoles = room.roles || []; roomRoles = room.roles || [];
} catch (error) { } catch (error) {
console.log('hasPermission -> Room not found'); console.log('hasPermission -> Room not found');
return permissions.reduce((result, permission) => { return permissions.map(() => false);
result[permission] = false;
return result;
}, {});
} }
// get permissions from database
try { try {
const permissionsFiltered = await permissionsCollection.query(Q.where('id', Q.oneOf(permissions))).fetch();
const shareUser = reduxStore.getState().share.user; const shareUser = reduxStore.getState().share.user;
const loginUser = reduxStore.getState().login.user; const loginUser = reduxStore.getState().login.user;
// get user roles on the server from redux // get user roles on the server from redux
const userRoles = (shareUser?.roles || loginUser?.roles) || []; const userRoles = (shareUser?.roles || loginUser?.roles) || [];
// merge both roles
const mergedRoles = [...new Set([...roomRoles, ...userRoles])]; const mergedRoles = [...new Set([...roomRoles, ...userRoles])];
return permissions.map(permission => permission.some(r => mergedRoles.includes(r) ?? false));
// return permissions in object format
// e.g. { 'edit-room': true, 'set-readonly': false }
return permissions.reduce((result, permission) => {
result[permission] = false;
const permissionFound = permissionsFiltered.find(p => p.id === permission);
if (permissionFound) {
result[permission] = returnAnArray(permissionFound.roles).some(r => mergedRoles.includes(r));
}
return result;
}, {});
} catch (e) { } catch (e) {
log(e); log(e);
} }
...@@ -1438,17 +1426,15 @@ const RocketChat = { ...@@ -1438,17 +1426,15 @@ const RocketChat = {
query, count, offset, sort query, count, offset, sort
}); });
}, },
async canAutoTranslate() { canAutoTranslate() {
const db = database.active;
try { try {
const AutoTranslate_Enabled = reduxStore.getState().settings && reduxStore.getState().settings.AutoTranslate_Enabled; const { AutoTranslate_Enabled } = reduxStore.getState().settings;
if (!AutoTranslate_Enabled) { if (!AutoTranslate_Enabled) {
return false; return false;
} }
const permissionsCollection = db.collections.get('permissions'); const autoTranslatePermission = reduxStore.getState().permissions['auto-translate'];
const autoTranslatePermission = await permissionsCollection.find('auto-translate'); const userRoles = (reduxStore.getState().login?.user?.roles) ?? [];
const userRoles = (reduxStore.getState().login.user && reduxStore.getState().login.user.roles) || []; return autoTranslatePermission?.some(role => userRoles.includes(role));
return autoTranslatePermission.roles.some(role => userRoles.includes(role));
} catch (e) { } catch (e) {
log(e); log(e);
return false; return false;
......
...@@ -18,6 +18,7 @@ import inviteLinks from './inviteLinks'; ...@@ -18,6 +18,7 @@ import inviteLinks from './inviteLinks';
import createDiscussion from './createDiscussion'; import createDiscussion from './createDiscussion';
import enterpriseModules from './enterpriseModules'; import enterpriseModules from './enterpriseModules';
import encryption from './encryption'; import encryption from './encryption';
import permissions from './permissions';
import inquiry from '../ee/omnichannel/reducers/inquiry'; import inquiry from '../ee/omnichannel/reducers/inquiry';
...@@ -41,5 +42,6 @@ export default combineReducers({ ...@@ -41,5 +42,6 @@ export default combineReducers({
createDiscussion, createDiscussion,
inquiry, inquiry,
enterpriseModules, enterpriseModules,
encryption encryption,
permissions
}); });
import { PERMISSIONS } from '../actions/actionsTypes';
const initialState = {
permissions: {}
};
export default function permissions(state = initialState, action) {
switch (action.type) {
case PERMISSIONS.SET:
return action.permissions;
default:
return state;
}
}
...@@ -124,6 +124,7 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch ...@@ -124,6 +124,7 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch
// and block the selectServerSuccess raising multiples errors // and block the selectServerSuccess raising multiples errors
RocketChat.setSettings(); RocketChat.setSettings();
RocketChat.setCustomEmojis(); RocketChat.setCustomEmojis();
RocketChat.setPermissions();
RocketChat.setEnterpriseModules(); RocketChat.setEnterpriseModules();
let serverInfo; let serverInfo;
......
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import reduxStore from '../lib/createStore';
const canPost = async({ rid }) => { const canPostReadOnly = async({ rid }) => {
try { // TODO: this is not reactive. If this permission changes, the component won't be updated
const permission = await RocketChat.hasPermission(['post-readonly'], rid); const postReadOnlyPermission = reduxStore.getState().permissions['post-readonly'];
return permission && permission['post-readonly']; const permission = await RocketChat.hasPermission([postReadOnlyPermission], rid);
} catch { return permission[0];
// do nothing
}
return false;
}; };
const isMuted = (room, user) => room && room.muted && room.muted.find && !!room.muted.find(m => m === user.username); const isMuted = (room, user) => room && room.muted && room.muted.find && !!room.muted.find(m => m === user.username);
...@@ -20,7 +18,7 @@ export const isReadOnly = async(room, user) => { ...@@ -20,7 +18,7 @@ export const isReadOnly = async(room, user) => {
return true; return true;
} }
if (room?.ro) { if (room?.ro) {
const allowPost = await canPost(room); const allowPost = await canPostReadOnly(room);
if (allowPost) { if (allowPost) {
return false; return false;
} }
......
...@@ -53,7 +53,15 @@ class RoomActionsView extends React.Component { ...@@ -53,7 +53,15 @@ class RoomActionsView extends React.Component {
closeRoom: PropTypes.func, closeRoom: PropTypes.func,
theme: PropTypes.string, theme: PropTypes.string,
fontScale: PropTypes.number, fontScale: PropTypes.number,
serverVersion: PropTypes.string serverVersion: PropTypes.string,
addUserToJoinedRoomPermission: PropTypes.array,
addUserToAnyCRoomPermission: PropTypes.array,
addUserToAnyPRoomPermission: PropTypes.array,
createInviteLinksPermission: PropTypes.array,
editRoomPermission: PropTypes.array,
toggleRoomE2EEncryptionPermission: PropTypes.array,
viewBroadcastMemberListPermission: PropTypes.array,
transferLivechatGuestPermission: PropTypes.array
} }
constructor(props) { constructor(props) {
...@@ -118,7 +126,7 @@ class RoomActionsView extends React.Component { ...@@ -118,7 +126,7 @@ class RoomActionsView extends React.Component {
this.updateRoomMember(); this.updateRoomMember();
} }
const canAutoTranslate = await RocketChat.canAutoTranslate(); const canAutoTranslate = RocketChat.canAutoTranslate();
this.setState({ canAutoTranslate }); this.setState({ canAutoTranslate });
this.canAddUser(); this.canAddUser();
...@@ -159,60 +167,62 @@ class RoomActionsView extends React.Component { ...@@ -159,60 +167,62 @@ class RoomActionsView extends React.Component {
canAddUser = async() => { canAddUser = async() => {
const { room, joined } = this.state; const { room, joined } = this.state;
const { addUserToJoinedRoomPermission, addUserToAnyCRoomPermission, addUserToAnyPRoomPermission } = this.props;
const { rid, t } = room; const { rid, t } = room;
let canAdd = false; let canAddUser = false;
const userInRoom = joined; const userInRoom = joined;
const permissions = await RocketChat.hasPermission(['add-user-to-joined-room', 'add-user-to-any-c-room', 'add-user-to-any-p-room'], rid); const permissions = await RocketChat.hasPermission([addUserToJoinedRoomPermission, addUserToAnyCRoomPermission, addUserToAnyPRoomPermission], rid);
if (permissions) { if (userInRoom && permissions[0]) {
if (userInRoom && permissions['add-user-to-joined-room']) { canAddUser = true;
canAdd = true; }
} if (t === 'c' && permissions[1]) {
if (t === 'c' && permissions['add-user-to-any-c-room']) { canAddUser = true;
canAdd = true;
}
if (t === 'p' && permissions['add-user-to-any-p-room']) {
canAdd = true;
}
} }
this.setState({ canAddUser: canAdd }); if (t === 'p' && permissions[2]) {
canAddUser = true;
}
this.setState({ canAddUser });
} }
canInviteUser = async() => { canInviteUser = async() => {
const { room } = this.state; const { room } = this.state;
const { createInviteLinksPermission } = this.props;
const { rid } = room; const { rid } = room;
const permissions = await RocketChat.hasPermission(['create-invite-links'], rid); const permissions = await RocketChat.hasPermission([createInviteLinksPermission], rid);
const canInviteUser = permissions && permissions['create-invite-links']; const canInviteUser = permissions[0];
this.setState({ canInviteUser }); this.setState({ canInviteUser });
} }
canEdit = async() => { canEdit = async() => {
const { room } = this.state; const { room } = this.state;
const { editRoomPermission } = this.props;
const { rid } = room; const { rid } = room;
const permissions = await RocketChat.hasPermission(['edit-room'], rid); const permissions = await RocketChat.hasPermission([editRoomPermission], rid);
const canEdit = permissions && permissions['edit-room']; const canEdit = permissions[0];
this.setState({ canEdit }); this.setState({ canEdit });
} }
canToggleEncryption = async() => { canToggleEncryption = async() => {
const { room } = this.state; const { room } = this.state;
const { toggleRoomE2EEncryptionPermission } = this.props;
const { rid } = room; const { rid } = room;
const permissions = await RocketChat.hasPermission(['toggle-room-e2e-encryption'], rid); const permissions = await RocketChat.hasPermission([toggleRoomE2EEncryptionPermission], rid);
const canToggleEncryption = permissions && permissions['toggle-room-e2e-encryption']; const canToggleEncryption = permissions[0];
this.setState({ canToggleEncryption }); this.setState({ canToggleEncryption });
} }
canViewMembers = async() => { canViewMembers = async() => {
const { room } = this.state; const { room } = this.state;